2"""Implementations to make access to SQLite databases a little more convenient.
5 Abstract class with which DB applications can be implemented.
7:py:obj:`SQLiteProperties`:
8 Class to manage properties stored in a database.
10Examplarical implementations based on :py:obj:`SQLiteAppl`:
12:py:obj:`searx.cache.ExpireCacheSQLite` :
13 Cache that manages key/value pairs in a SQLite DB, in which the key/value
14 pairs are deleted after an "expire" time. This type of cache is used, for
15 example, for the engines, see :py:obj:`searx.enginelib.EngineCache`.
17:py:obj:`searx.favicons.cache.FaviconCacheSQLite` :
18 Favicon cache that manages the favicon BLOBs in a SQLite DB.
22from __future__
import annotations
32from searx
import logger
34logger = logger.getChild(
"sqlitedb")
36THREAD_LOCAL = threading.local()
40 """A *thead-local* DB session"""
44 """Returns a thread local DB connection. The connection is only
45 established once per thread.
47 if getattr(THREAD_LOCAL,
"DBSession_map",
None)
is None:
48 THREAD_LOCAL.DBSession_map = {}
50 session = THREAD_LOCAL.DBSession_map.get(app.db_url)
60 if getattr(THREAD_LOCAL,
"DBSession_map",
None)
is None:
61 THREAD_LOCAL.DBSession_map = {}
62 THREAD_LOCAL.DBSession_map[self.
app.db_url] = self
65 def conn(self) -> sqlite3.Connection:
66 msg = f
"[{threading.current_thread().ident}] DBSession: " f
"{self.app.__class__.__name__}({self.app.db_url})"
67 if self.
_conn is None:
69 logger.debug(
"%s --> created new connection", msg)
77 if self.
_conn is not None:
90 """Abstract base class for implementing convenient DB access in SQLite
91 applications. In the constructor, a :py:obj:`SQLiteProperties` instance is
92 already aggregated under ``self.properties``."""
94 DDL_CREATE_TABLES: dict[str, str] = {}
97 """As soon as changes are made to the DB schema, the version number must be
98 increased. Changes to the version number require the DB to be recreated (or
99 migrated / if an migration path exists and is implemented)."""
101 SQLITE_THREADING_MODE = {
104 3:
"serialized"}[sqlite3.threadsafety]
105 """Threading mode of the SQLite library. Depends on the options used at
106 compile time and is different for different distributions and architectures.
108 Possible values are 0:``single-thread``, 1:``multi-thread``,
109 3:``serialized`` (see :py:obj:`sqlite3.threadsafety`). Pre- Python 3.11
110 this value was hard coded to 1.
112 Depending on this value, optimizations are made, e.g. in “serialized” mode
113 it is not necessary to create a separate DB connector for each thread.
116 SQLITE_JOURNAL_MODE =
"WAL"
117 """``SQLiteAppl`` applications are optimized for WAL_ mode, its not recommend
118 to change the journal mode (see :py:obj:`SQLiteAppl.tear_down`).
120 .. _WAL: https://sqlite.org/wal.html
122 SQLITE_CONNECT_ARGS = {
125 "check_same_thread": bool(SQLITE_THREADING_MODE !=
"serialized"),
126 "cached_statements": 0,
128 "isolation_level":
None,
130 """Connection arguments (:py:obj:`sqlite3.connect`)
132 ``check_same_thread``:
133 Is disabled by default when :py:obj:`SQLITE_THREADING_MODE` is
134 ``serialized``. The check is more of a hindrance in this case because it
135 would prevent a DB connector from being used in multiple threads.
137 ``cached_statements``:
138 Is set to ``0`` by default. Note: Python 3.12+ fetch result are not
139 consistent in multi-threading application and causing an API misuse error.
141 The multithreading use in SQLiteAppl is intended and supported if
142 threadsafety is set to 3 (aka "serialized"). CPython supports “serialized”
143 from version 3.12 on, but unfortunately only with errors:
145 - https://github.com/python/cpython/issues/118172
146 - https://github.com/python/cpython/issues/123873
148 The workaround for SQLite3 multithreading cache inconsistency is to set
149 option ``cached_statements`` to ``0`` by default.
174 f
"SQLite library is compiled with {self.SQLITE_THREADING_MODE} mode,"
175 " read https://docs.python.org/3/library/sqlite3.html#sqlite3.threadsafety"
177 if threading.active_count() > 1:
182 if sqlite3.sqlite_version_info <= (3, 35):
185 "SQLite runtime library version %s is not supported (require >= 3.35)", sqlite3.sqlite_version
190 conn.execute(f
"PRAGMA journal_mode={self.SQLITE_JOURNAL_MODE}")
195 """Creates a new DB connection (:py:obj:`SQLITE_CONNECT_ARGS`). If not
196 already done, the DB schema is set up. The caller must take care of
197 closing the resource. Alternatively, :py:obj:`SQLiteAppl.DB` can also
198 be used (the resource behind `self.DB` is automatically closed when the
199 process or thread is terminated).
201 if sys.version_info < (3, 12):
206 f
"[{threading.current_thread().ident}] {self.__class__.__name__}({self.db_url})"
207 f
" {self.SQLITE_CONNECT_ARGS} // {self.SQLITE_JOURNAL_MODE}"
216 """Create user-defined_ SQL functions.
218 ``REGEXP(<pattern>, <field>)`` : 0 | 1
219 `re.search`_ returns (int) 1 for a match and 0 for none match of
220 ``<pattern>`` in ``<field>``.
224 SELECT '12' AS field WHERE REGEXP('^[0-9][0-9]$', field)
227 SELECT REGEXP('[0-9][0-9]', 'X12Y')
229 SELECT REGEXP('[0-9][0-9]', 'X1Y')
232 .. _user-defined: https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function
233 .. _deterministic: https://sqlite.org/deterministic.html
234 .. _re.search: https://docs.python.org/3/library/re.html#re.search
237 conn.create_function(
"regexp", 2,
lambda x, y: 1
if re.search(x, y)
else 0, deterministic=
True)
240 def DB(self) -> sqlite3.Connection:
241 """Provides a DB connection. The connection is a *singleton* and
242 therefore well suited for read access. If
243 :py:obj:`SQLITE_THREADING_MODE` is ``serialized`` only one DB connection
244 is created for all threads.
248 For dedicated `transaction control`_, it is recommended to create a
249 new connection (:py:obj:`SQLiteAppl.connect`).
251 .. _transaction control:
252 https://docs.python.org/3/library/sqlite3.html#sqlite3-controlling-transactions
267 conn = DBSession.get_connect(self)
269 conn = DBSession.get_connect(self)
278 def init(self, conn: sqlite3.Connection) -> bool:
279 """Initializes the DB schema and properties, is only executed once even
280 if called several times.
282 If the initialization has not yet taken place, it is carried out and a
283 `True` is returned to the caller at the end. If the initialization has
284 already been carried out in the past, `False` is returned.
291 logger.debug(
"init DB: %s", self.
db_url)
301 raise sqlite3.DatabaseError(
"Expected DB schema v%s, DB schema is v%s" % (self.
DB_SCHEMA, ver))
302 logger.debug(
"DB_SCHEMA = %s", ver)
308 logger.debug(
"create schema ..")
314 self.
properties.set(f
"Table {table_name} created", table_name)
318 """Simple class to manage properties of a DB application in the DB. The
319 object has its own DB connection and transaction area.
323 CREATE TABLE IF NOT EXISTS properties (
326 m_time INTEGER DEFAULT (strftime('%s', 'now')),
331 SQLITE_JOURNAL_MODE =
"WAL"
333 DDL_PROPERTIES =
"""\
334CREATE TABLE IF NOT EXISTS properties (
337 m_time INTEGER DEFAULT (strftime('%s', 'now')), -- last modified (unix epoch) time in sec.
338 PRIMARY KEY (name))"""
340 """Table to store properties of the DB application"""
342 SQL_GET =
"SELECT value FROM properties WHERE name = ?"
343 SQL_M_TIME =
"SELECT m_time FROM properties WHERE name = ?"
345 "INSERT INTO properties (name, value) VALUES (?, ?)"
346 " ON CONFLICT(name) DO UPDATE"
347 " SET value=excluded.value, m_time=strftime('%s', 'now')"
349 SQL_DELETE =
"DELETE FROM properties WHERE name = ?"
351 "SELECT name FROM sqlite_master"
352 " WHERE type='table' AND name='properties'"
354 SQLITE_CONNECT_ARGS = dict(SQLiteAppl.SQLITE_CONNECT_ARGS)
362 def init(self, conn: sqlite3.Connection) -> bool:
363 """Initializes DB schema of the properties in the DB."""
368 logger.debug(
"init properties of DB: %s", self.
db_url)
370 if res.fetchone()
is None:
375 """Returns the value of the property ``name`` or ``default`` if property
378 res = self.
DB.execute(self.
SQL_GET, (name,)).fetchone()
383 def set(self, name: str, value: str | int):
384 """Set ``value`` of property ``name`` in DB. If property already
385 exists, update the ``m_time`` (and the value)."""
388 self.
DB.execute(self.
SQL_SET, (name, value))
391 """Delete of property ``name`` from DB."""
396 def row(self, name: str, default=
None):
397 """Returns the DB row of property ``name`` or ``default`` if property
400 res = self.
DB.execute(
"SELECT * FROM properties WHERE name = ?", (name,))
405 col_names = [column[0]
for column
in row.description]
406 return dict(zip(col_names, row))
408 def m_time(self, name: str, default: int = 0) -> int:
409 """Last modification time of this property."""
422 for row
in self.
DB.execute(
"SELECT name, value, m_time FROM properties"):
423 name, value, m_time = row
424 m_time = datetime.datetime.fromtimestamp(m_time).strftime(
"%Y-%m-%d %H:%M:%S")
425 lines.append(f
"[last modified: {m_time}] {name:20s}: {value}")
426 return "\n".join(lines)
__init__(self, SQLiteAppl app)
sqlite3.Connection get_connect(cls, SQLiteAppl app)
sqlite3.Connection conn(self)
sqlite3.Connection _connect(self)
sqlite3.Connection connect(self)
register_functions(self, conn)
bool init(self, sqlite3.Connection conn)
create_schema(self, sqlite3.Connection conn)
dict SQLITE_THREADING_MODE
sqlite3.Connection DB(self)
row(self, str name, default=None)
int delete(self, str name)
set(self, str name, str|int value)
create_schema(self, conn)
int m_time(self, str name, int default=0)
__call__(self, str name, default=None)
__init__(self, str db_url)
bool init(self, sqlite3.Connection conn)