""" Android SQLite 组件 — SQLiteDatabase, SQLiteOpenHelper, SQLiteStatement 等 使用 Python sqlite3 模块作为后端 方法签名完全对齐 Android SDK """ import os import sys import threading import sqlite3 as _sqlite3 from typing import Any, Dict, List, Optional, Set, Tuple, Union from .._core import ( Stub, AndroidOnlyError, android_context, resolve_android_path, to_android_path, _sdcard_root, _app_data_dir, _package_name, ) from .database import ( Cursor, AbstractCursor, ContentObserver, DataSetObserver, SQLException as BaseSQLException, ) # ═══════════════════════════════════════════════════════════════ # SQLiteException hierarchy # ═══════════════════════════════════════════════════════════════ class SQLiteException(BaseSQLException): """SQLiteException — SQLite 异常基类""" def __init__(self, message: str = ""): super().__init__(message) self._error_message = message class SQLiteAbortException(SQLiteException): pass class SQLiteAccessPermException(SQLiteException): pass class SQLiteBindOrColumnIndexOutOfRangeException(SQLiteException): pass class SQLiteBlobTooBigException(SQLiteException): pass class SQLiteCantOpenDatabaseException(SQLiteException): pass class SQLiteConstraintException(SQLiteException): pass class SQLiteDatabaseCorruptException(SQLiteException): pass class SQLiteDatabaseLockedException(SQLiteException): pass class SQLiteDatatypeMismatchException(SQLiteException): pass class SQLiteDiskIOException(SQLiteException): pass class SQLiteDoneException(SQLiteException): pass class SQLiteException_General(SQLiteException): pass class SQLiteFullException(SQLiteException): pass class SQLiteMisuseException(SQLiteException): pass class SQLiteOutOfMemoryException(SQLiteException): pass class SQLiteReadOnlyDatabaseException(SQLiteException): pass class SQLiteTableLockedException(SQLiteException): pass class SQLiteUnsupportedOperationException(SQLiteException): pass # ═══════════════════════════════════════════════════════════════ # SQLiteClosable # ═══════════════════════════════════════════════════════════════ class SQLiteClosable(Stub): """SQLiteClosable — 可关闭资源基类""" def __init__(self): super().__init__() self._closed = False self._reference_count = 1 def acquireReference(self): if self._closed: raise IllegalStateException("already closed") self._reference_count += 1 def releaseReference(self): self._reference_count -= 1 if self._reference_count <= 0: self.onAllReferencesReleased() def releaseReferenceFromContainer(self): self.releaseReference() def onAllReferencesReleased(self): self.close() def close(self): self._closed = True def isClosed(self) -> bool: return self._closed class IllegalStateException(RuntimeError): pass # ═══════════════════════════════════════════════════════════════ # SQLiteProgram # ═══════════════════════════════════════════════════════════════ class SQLiteProgram(SQLiteClosable): """SQLiteProgram — SQL 程序基类""" def __init__(self, db: "SQLiteDatabase", sql: str): super().__init__() self._db = db self._sql = sql self._bindings: Dict[int, Any] = {} self._next_bind_index = 1 def getDatabase(self) -> "SQLiteDatabase": return self._db def getSql(self) -> str: return self._sql def bindNull(self, index: int): self._bindings[index] = None def bindLong(self, index: int, value: int): self._bindings[index] = value def bindDouble(self, index: int, value: float): self._bindings[index] = value def bindString(self, index: int, value: str): self._bindings[index] = value def bindBlob(self, index: int, value: bytes): self._bindings[index] = value def clearBindings(self): self._bindings.clear() def _bind_parameters(self, cursor: _sqlite3.Cursor): for idx, val in sorted(self._bindings.items()): cursor.execute(f"PRAGMA dummy={idx}") # ═══════════════════════════════════════════════════════════════ # SQLiteStatement # ═══════════════════════════════════════════════════════════════ class SQLiteStatement(SQLiteProgram): """SQLiteStatement — 预编译语句""" def __init__(self, db: "SQLiteDatabase", sql: str): super().__init__(db, sql) def execute(self): try: conn = self._db._acquire_connection() cursor = conn.execute(self._sql, list(self._bindings.values())) conn.commit() except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), self._sql) def executeUpdateDelete(self) -> int: try: conn = self._db._acquire_connection() cursor = conn.execute(self._sql, list(self._bindings.values())) conn.commit() return cursor.rowcount except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), self._sql) def executeInsert(self) -> int: try: conn = self._db._acquire_connection() cursor = conn.execute(self._sql, list(self._bindings.values())) conn.commit() return cursor.lastrowid except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), self._sql) def simpleQueryForLong(self) -> int: try: conn = self._db._acquire_connection() cursor = conn.execute(self._sql, list(self._bindings.values())) row = cursor.fetchone() conn.commit() if row: return int(row[0]) if row[0] is not None else 0 raise SQLiteDoneException("no results") except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), self._sql) def simpleQueryForString(self) -> Optional[str]: try: conn = self._db._acquire_connection() cursor = conn.execute(self._sql, list(self._bindings.values())) row = cursor.fetchone() conn.commit() if row: return str(row[0]) if row[0] is not None else None raise SQLiteDoneException("no results") except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), self._sql) # ═══════════════════════════════════════════════════════════════ # SQLiteQuery # ═══════════════════════════════════════════════════════════════ class SQLiteQuery(SQLiteProgram): """SQLiteQuery — 查询对象""" def __init__(self, db: "SQLiteDatabase", sql: str): super().__init__(db, sql) def toString(self) -> str: return f"SQLiteQuery: {self._sql}" def sql(self) -> str: return self._sql def bind(self, index: int, value: Any): self._bindings[index] = value # ═══════════════════════════════════════════════════════════════ # SQLiteCursor # ═══════════════════════════════════════════════════════════════ class SQLiteCursor(AbstractCursor): """SQLiteCursor — SQLite 游标实现""" def __init__(self, db: "SQLiteDatabase", query: SQLiteQuery): super().__init__() self._db = db self._query = query self._column_names: List[str] = [] self._rows: List[List[Any]] = [] self._current_row: Optional[List[Any]] = None self._loaded = False self._closed = False def _load(self): if self._loaded or self._closed: return self._loaded = True try: conn = self._db._acquire_connection() cursor = conn.execute(self._query.getSql(), list(self._query._bindings.values())) self._column_names = [desc[0] for desc in cursor.description] if cursor.description else [] self._rows = cursor.fetchall() except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), self._query.getSql()) def getCount(self) -> int: self._load() return len(self._rows) def getColumnNames(self) -> List[str]: self._load() return list(self._column_names) def getColumnIndex(self, column_name: str) -> int: self._load() try: return self._column_names.index(column_name) except ValueError: return -1 def moveToPosition(self, position: int) -> bool: self._load() if position < -1 or position >= len(self._rows): return False self._pos = position if 0 <= position < len(self._rows): self._current_row = self._rows[position] else: self._current_row = None return True def getString(self, column_index: int) -> Optional[str]: if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): return None v = self._current_row[column_index] return str(v) if v is not None else None def getInt(self, column_index: int) -> int: if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): return 0 v = self._current_row[column_index] if v is None: return 0 try: return int(v) except (ValueError, TypeError): return 0 def getLong(self, column_index: int) -> int: return self.getInt(column_index) def getFloat(self, column_index: int) -> float: if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): return 0.0 v = self._current_row[column_index] if v is None: return 0.0 try: return float(v) except (ValueError, TypeError): return 0.0 def getDouble(self, column_index: int) -> float: return self.getFloat(column_index) def getShort(self, column_index: int) -> int: return self.getInt(column_index) def getBlob(self, column_index: int) -> Optional[bytes]: if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): return None v = self._current_row[column_index] if isinstance(v, bytes): return v if v is not None: return bytes(str(v), "utf-8") return None def getType(self, column_index: int) -> int: self._load() if self._current_row is None or column_index < 0 or column_index >= len(self._current_row): return Cursor.FIELD_TYPE_NULL v = self._current_row[column_index] if v is None: return Cursor.FIELD_TYPE_NULL if isinstance(v, int): return Cursor.FIELD_TYPE_INTEGER if isinstance(v, float): return Cursor.FIELD_TYPE_FLOAT if isinstance(v, bytes): return Cursor.FIELD_TYPE_BLOB return Cursor.FIELD_TYPE_STRING def isNull(self, column_index: int) -> bool: return self.getType(column_index) == Cursor.FIELD_TYPE_NULL def close(self): self._closed = True self._rows.clear() self._current_row = None def requery(self) -> bool: self._loaded = False self._rows.clear() self._current_row = None self._pos = -1 return True # ═══════════════════════════════════════════════════════════════ # SQLiteQueryBuilder # ═══════════════════════════════════════════════════════════════ class SQLiteQueryBuilder(Stub): """SQLiteQueryBuilder — 查询构建器""" def __init__(self): super().__init__() self._tables: str = "" self._distinct: bool = False self._where: Optional[str] = None def setTables(self, tables: str): self._tables = tables def getTables(self) -> str: return self._tables def appendWhere(self, where: str): if self._where: self._where = f"({self._where}) AND ({where})" else: self._where = where def appendWhereStandalone(self, where: str): self._where = where def setDistinct(self, distinct: bool): self._distinct = distinct def buildQueryString(self, distinct: bool, tables: str, columns: Optional[List[str]], where: Optional[str], group_by: Optional[str], having: Optional[str], order_by: Optional[str], limit: Optional[str]) -> str: sql = "SELECT " if distinct: sql += "DISTINCT " if columns and len(columns) > 0: sql += ", ".join(columns) else: sql += "*" sql += f" FROM {tables}" if where: sql += f" WHERE ({where})" if group_by: sql += f" GROUP BY {group_by}" if having: sql += f" HAVING {having}" if order_by: sql += f" ORDER BY {order_by}" if limit: sql += f" LIMIT {limit}" return sql def buildQuery(self, columns: Optional[List[str]], where: Optional[str], group_by: Optional[str], having: Optional[str], order_by: Optional[str], limit: Optional[str]) -> str: return self.buildQueryString(self._distinct, self._tables, columns, where or self._where, group_by, having, order_by, limit) def query(self, db: "SQLiteDatabase", columns: Optional[List[str]], where: Optional[str], group_by: Optional[str], having: Optional[str], order_by: Optional[str], limit: Optional[str]) -> SQLiteCursor: sql = self.buildQuery(columns, where, group_by, having, order_by, limit) return db.rawQuery(sql, None) # ═══════════════════════════════════════════════════════════════ # SQLiteDatabase # ═══════════════════════════════════════════════════════════════ def _map_sqlite_error(msg: str, sql: str = "") -> SQLiteException: msg_lower = msg.lower() if "constraint" in msg_lower: return SQLiteConstraintException(msg) if "locked" in msg_lower: return SQLiteDatabaseLockedException(msg) if "corrupt" in msg_lower: return SQLiteDatabaseCorruptException(msg) if "readonly" in msg_lower or "read only" in msg_lower: return SQLiteReadOnlyDatabaseException(msg) if "disk" in msg_lower or "full" in msg_lower: return SQLiteDiskIOException(msg) if "disk" in msg_lower else SQLiteFullException(msg) if "abort" in msg_lower: return SQLiteAbortException(msg) if "permission" in msg_lower: return SQLiteAccessPermException(msg) if "out of memory" in msg_lower: return SQLiteOutOfMemoryException(msg) if "not a database" in msg_lower or "cantopen" in msg_lower: return SQLiteCantOpenDatabaseException(msg) if "misuse" in msg_lower: return SQLiteMisuseException(msg) return SQLiteException(msg) class SQLiteDatabase(SQLiteClosable): """SQLiteDatabase — 数据库操作类""" CONFLICT_ROLLBACK = 1 CONFLICT_ABORT = 2 CONFLICT_FAIL = 3 CONFLICT_IGNORE = 4 CONFLICT_REPLACE = 5 OPEN_READWRITE = 0 OPEN_READONLY = 1 CREATE_IF_NECESSARY = 268435456 NO_LOCALIZED_COLLATORS = 16 _db_registry: Dict[str, "SQLiteDatabase"] = {} _lock = threading.Lock() def __init__(self, path: str, flags: int = 0): super().__init__() self._path = path self._flags = flags self._conn: Optional[_sqlite3.Connection] = None self._in_transaction = False self._transaction_successful = False self._version: int = 0 self._max_size: int = 0 self._page_size: int = 4096 self._enable_wal = False self._attached_dbs: List[Dict[str, Any]] = [] @staticmethod def openDatabase(path: str, cursor_factory: Any = None, flags: int = 0, error_handler: Any = None) -> "SQLiteDatabase": db = SQLiteDatabase(path, flags) try: db._conn = _sqlite3.connect(path) db._conn.execute("PRAGMA journal_mode=WAL") conn_cursor = db._conn.execute("PRAGMA schema_version") row = conn_cursor.fetchone() if row: db._version = row[0] except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e)) SQLiteDatabase._db_registry[path] = db return db @staticmethod def openOrCreateDatabase(path: str, cursor_factory: Any = None, error_handler: Any = None) -> "SQLiteDatabase": return SQLiteDatabase.openDatabase(path, cursor_factory, SQLiteDatabase.CREATE_IF_NECESSARY, error_handler) @staticmethod def create(cursor_factory: Any = None, error_handler: Any = None) -> "SQLiteDatabase": import tempfile tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) tmp.close() return SQLiteDatabase.openOrCreateDatabase(tmp.name, cursor_factory, error_handler) @staticmethod def deleteDatabase(path: str) -> bool: try: if path in SQLiteDatabase._db_registry: db = SQLiteDatabase._db_registry[path] db.close() del SQLiteDatabase._db_registry[path] os.remove(path) wal_path = path + "-wal" if os.path.exists(wal_path): os.remove(wal_path) shm_path = path + "-shm" if os.path.exists(shm_path): os.remove(shm_path) return True except (OSError, Exception): return False @staticmethod def databaseList() -> List[str]: return list(SQLiteDatabase._db_registry.keys()) def _acquire_connection(self) -> _sqlite3.Connection: if self._conn is None: raise SQLiteCantOpenDatabaseException("Database not open") return self._conn def execSQL(self, sql: str, bind_args: Optional[List[Any]] = None): self._ensure_open() try: conn = self._acquire_connection() args = bind_args or [] conn.execute(sql, args) if not self._in_transaction: conn.commit() except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), sql) def rawQuery(self, sql: str, selection_args: Optional[List[Any]] = None) -> SQLiteCursor: self._ensure_open() query = SQLiteQuery(self, sql) if selection_args: for i, arg in enumerate(selection_args): query.bind(i + 1, arg) cursor = SQLiteCursor(self, query) return cursor def insert(self, table: str, null_column_hack: Optional[str], values: Any) -> int: self._ensure_open() if values is None: return -1 cols = [] vals = [] if hasattr(values, "valueSet"): items = list(values.valueSet()) elif hasattr(values, "key_set"): items = [(k, getattr(values, f"get{k[0].upper()}{k[1:]}", lambda: None)()) for k in values.key_set()] elif isinstance(values, dict): items = list(values.items()) else: return -1 for k, v in items: cols.append(str(k)) vals.append(v) if len(cols) == 0: cols = ["_id"] vals = [None] col_str = ", ".join(cols) placeholders = ", ".join(["?"] * len(vals)) sql = f"INSERT INTO {table} ({col_str}) VALUES ({placeholders})" try: conn = self._acquire_connection() cursor = conn.execute(sql, vals) if not self._in_transaction: conn.commit() return cursor.lastrowid except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), sql) def update(self, table: str, values: Any, where_clause: Optional[str] = None, where_args: Optional[List[Any]] = None) -> int: self._ensure_open() if values is None: return 0 set_pairs = [] vals = [] if isinstance(values, dict): items = list(values.items()) elif hasattr(values, "valueSet"): items = list(values.valueSet()) else: return 0 for k, v in items: set_pairs.append(f"{k} = ?") vals.append(v) set_str = ", ".join(set_pairs) sql = f"UPDATE {table} SET {set_str}" if where_clause: sql += f" WHERE {where_clause}" if where_args: vals.extend(where_args) try: conn = self._acquire_connection() cursor = conn.execute(sql, vals) if not self._in_transaction: conn.commit() return cursor.rowcount except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), sql) def delete(self, table: str, where_clause: Optional[str] = None, where_args: Optional[List[Any]] = None) -> int: self._ensure_open() sql = f"DELETE FROM {table}" vals: List[Any] = [] if where_clause: sql += f" WHERE {where_clause}" if where_args: vals = list(where_args) try: conn = self._acquire_connection() cursor = conn.execute(sql, vals) if not self._in_transaction: conn.commit() return cursor.rowcount except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e), sql) def query(self, table: str, columns: Optional[List[str]] = None, selection: Optional[str] = None, selection_args: Optional[List[Any]] = None, group_by: Optional[str] = None, having: Optional[str] = None, order_by: Optional[str] = None, limit: Optional[str] = None) -> SQLiteCursor: col_str = ", ".join(columns) if columns else "*" sql = f"SELECT {col_str} FROM {table}" if selection: sql += f" WHERE {selection}" if group_by: sql += f" GROUP BY {group_by}" if having: sql += f" HAVING {having}" if order_by: sql += f" ORDER BY {order_by}" if limit: sql += f" LIMIT {limit}" return self.rawQuery(sql, selection_args) def compileStatement(self, sql: str) -> SQLiteStatement: return SQLiteStatement(self, sql) def beginTransaction(self): self._ensure_open() try: conn = self._acquire_connection() conn.execute("BEGIN TRANSACTION") self._in_transaction = True self._transaction_successful = False except _sqlite3.DatabaseError as e: raise _map_sqlite_error(str(e)) def endTransaction(self): if self._in_transaction and self._conn: try: if not self._transaction_successful: self._conn.rollback() else: self._conn.commit() except _sqlite3.DatabaseError: pass self._in_transaction = False def setTransactionSuccessful(self): if not self._in_transaction: raise IllegalStateException("Not in transaction") self._transaction_successful = True def inTransaction(self) -> bool: return self._in_transaction def isOpen(self) -> bool: return self._conn is not None def getPath(self) -> str: return self._path def getVersion(self) -> int: return self._version def setVersion(self, version: int): self._version = version if self._conn: try: self._conn.execute(f"PRAGMA schema_version = {version}") except _sqlite3.DatabaseError: pass def onCreate(self, db: "SQLiteDatabase"): pass def onConfigure(self, db: "SQLiteDatabase"): pass def enableWriteAheadLogging(self) -> bool: self._ensure_open() try: self._conn.execute("PRAGMA journal_mode=WAL") self._enable_wal = True return True except _sqlite3.DatabaseError: return False def disableWriteAheadLogging(self): try: self._conn.execute("PRAGMA journal_mode=DELETE") self._enable_wal = False except _sqlite3.DatabaseError: pass def isWriteAheadLoggingEnabled(self) -> bool: return self._enable_wal def getMaximumSize(self) -> int: return self._max_size def getPageSize(self) -> int: return self._page_size def setMaximumSize(self, size: int) -> int: self._max_size = size return size def setPageSize(self, size: int): self._page_size = size if self._conn: try: self._conn.execute(f"PRAGMA page_size = {size}") except _sqlite3.DatabaseError: pass def getPageCount(self) -> int: if self._conn: try: cursor = self._conn.execute("PRAGMA page_count") row = cursor.fetchone() if row: return row[0] except _sqlite3.DatabaseError: pass return 0 def yieldIfContendedSafely(self, sleep_time: int = 0) -> bool: return True def needUpgrade(self, new_version: int) -> bool: return new_version > self._version def getAttachedDbs(self) -> Optional[List[Dict[str, Any]]]: return self._attached_dbs def isReadOnly(self) -> bool: return bool(self._flags & self.OPEN_READONLY) def close(self): if self._conn: try: self._conn.close() except Exception: pass self._conn = None super().close() def _ensure_open(self): if self._conn is None: raise SQLiteCantOpenDatabaseException("Database not open") def toString(self) -> str: return f"SQLiteDatabase(path={self._path})" # ═══════════════════════════════════════════════════════════════ # SQLiteOpenHelper # ═══════════════════════════════════════════════════════════════ class SQLiteOpenHelper(Stub): """SQLiteOpenHelper — 数据库打开辅助类""" def __init__(self, context: Any, name: Optional[str], factory: Any, version: int, error_handler: Any = None): super().__init__() self._context = context self._name = name self._factory = factory self._version = version self._error_handler = error_handler self._readable_db: Optional[SQLiteDatabase] = None self._writable_db: Optional[SQLiteDatabase] = None self._database_path: Optional[str] = None self._is_initialized = False def _get_database_path(self) -> str: if self._database_path: return self._database_path db_dir = os.path.join(_app_data_dir, "databases") os.makedirs(db_dir, exist_ok=True) if self._name: self._database_path = os.path.join(db_dir, self._name) else: import tempfile tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) tmp.close() self._database_path = tmp.name return self._database_path def getWritableDatabase(self) -> SQLiteDatabase: if self._writable_db and self._writable_db.isOpen(): return self._writable_db path = self._get_database_path() db = SQLiteDatabase.openOrCreateDatabase(path, self._factory, self._error_handler) if not self._is_initialized: self._is_initialized = True current_version = db.getVersion() if current_version == 0: self.onCreate(db) db.setVersion(self._version) elif current_version < self._version: self.onUpgrade(db, current_version, self._version) elif current_version > self._version: self.onDowngrade(db, current_version, self._version) self.onOpen(db) self._writable_db = db self._readable_db = db return db def getReadableDatabase(self) -> SQLiteDatabase: return self.getWritableDatabase() def onCreate(self, db: SQLiteDatabase): pass def onUpgrade(self, db: SQLiteDatabase, old_version: int, new_version: int): pass def onDowngrade(self, db: SQLiteDatabase, old_version: int, new_version: int): pass def onOpen(self, db: SQLiteDatabase): pass def close(self): if self._writable_db: self._writable_db.close() self._writable_db = None if self._readable_db: self._readable_db = None def getDatabaseName(self) -> Optional[str]: return self._name def getReadableDB(self) -> Optional[SQLiteDatabase]: return self._readable_db def getWriteableDB(self) -> Optional[SQLiteDatabase]: return self._writable_db def toString(self) -> str: return f"SQLiteOpenHelper(name={self._name}, version={self._version})" # ═══════════════════════════════════════════════════════════════ # __all__ # ═══════════════════════════════════════════════════════════════ __all__ = [ "SQLiteDatabase", "SQLiteOpenHelper", "SQLiteStatement", "SQLiteCursor", "SQLiteQuery", "SQLiteQueryBuilder", "SQLiteClosable", "SQLiteProgram", "SQLiteAbortException", "SQLiteAccessPermException", "SQLiteBindOrColumnIndexOutOfRangeException", "SQLiteBlobTooBigException", "SQLiteCantOpenDatabaseException", "SQLiteConstraintException", "SQLiteDatabaseCorruptException", "SQLiteDatabaseLockedException", "SQLiteDatatypeMismatchException", "SQLiteDiskIOException", "SQLiteDoneException", "SQLiteException", "SQLiteFullException", "SQLiteMisuseException", "SQLiteOutOfMemoryException", "SQLiteReadOnlyDatabaseException", "SQLiteTableLockedException", "SQLiteUnsupportedOperationException", ]