hivemind-sqlite-database 0.4.2a1__tar.gz → 0.4.3a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hivemind-sqlite-database might be problematic. Click here for more details.

Files changed (14) hide show
  1. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/PKG-INFO +1 -1
  2. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database/__init__.py +40 -8
  3. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database/version.py +2 -2
  4. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database.egg-info/PKG-INFO +1 -1
  5. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/tests/test_sqlitedb.py +50 -0
  6. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/LICENSE.md +0 -0
  7. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/README.md +0 -0
  8. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database.egg-info/SOURCES.txt +0 -0
  9. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database.egg-info/dependency_links.txt +0 -0
  10. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database.egg-info/entry_points.txt +0 -0
  11. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database.egg-info/requires.txt +0 -0
  12. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/hivemind_sqlite_database.egg-info/top_level.txt +0 -0
  13. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/pyproject.toml +0 -0
  14. {hivemind_sqlite_database-0.4.2a1 → hivemind_sqlite_database-0.4.3a2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hivemind-sqlite-database
3
- Version: 0.4.2a1
3
+ Version: 0.4.3a2
4
4
  Summary: sqlite database plugin for hivemind-core
5
5
  Author-email: jarbasAi <jarbasai@mailfence.com>
6
6
  License: Apache-2.0
@@ -26,6 +26,13 @@ class SQLiteDB(AbstractDB):
26
26
  name: str = "clients"
27
27
  subfolder: str = "hivemind-core"
28
28
  password: Optional[str] = None
29
+ # Overrides the computed xdg path entirely, e.g. ":memory:" for tests.
30
+ # Every thread's connection is opened against this same target, so
31
+ # nothing ever silently falls back to the real client database.
32
+ # ":memory:" becomes a named shared in-memory database, one per
33
+ # SQLiteDB instance, so worker threads see the same tables as the
34
+ # thread that created them.
35
+ db_path: Optional[str] = None
29
36
 
30
37
  # How long SQLite waits for a file lock held by another connection
31
38
  # before giving up with SQLITE_BUSY, in milliseconds.
@@ -51,9 +58,22 @@ class SQLiteDB(AbstractDB):
51
58
  up as ``sqlite3.ProgrammingError: bad parameter or other API
52
59
  misuse`` and as writes that land with corrupted bindings.
53
60
  """
54
- self._db_path = os.path.join(xdg_data_home(), self.subfolder, self.name + ".db")
61
+ if self.db_path is not None:
62
+ self._db_path = self.db_path
63
+ if self._db_path == ":memory:":
64
+ # A plain ":memory:" database belongs to the one connection
65
+ # that opened it, so the next thread would open a second,
66
+ # empty database and find no tables. Give it a name and a
67
+ # shared cache instead. The name carries the instance id, so
68
+ # two in-memory databases in one process stay independent.
69
+ self._db_path = f"file:hivemind-{id(self):x}?mode=memory&cache=shared"
70
+ else:
71
+ self._db_path = os.path.join(xdg_data_home(), self.subfolder, self.name + ".db")
72
+ self._is_uri = self._db_path.startswith("file:")
73
+ parent = os.path.dirname(self._db_path)
74
+ if parent and not self._is_uri:
75
+ os.makedirs(parent, exist_ok=True)
55
76
  LOG.debug(f"sqlite database path: {self._db_path}")
56
- os.makedirs(os.path.dirname(self._db_path), exist_ok=True)
57
77
 
58
78
  if self.password is not None and self.password == "":
59
79
  raise ValueError("password must be non-empty when encryption is enabled")
@@ -61,6 +81,11 @@ class SQLiteDB(AbstractDB):
61
81
  self._write_lock = threading.Lock()
62
82
  # opening the first connection also applies the WAL pragma
63
83
  self.conn.execute("PRAGMA journal_mode=WAL")
84
+ if "mode=memory" in self._db_path:
85
+ # A shared in-memory database lives only while a connection to
86
+ # it is open. Hold one for the lifetime of this object so the
87
+ # tables survive a thread closing its own connection.
88
+ self._keepalive = self._connect()
64
89
  self._initialize_database()
65
90
  self._maybe_migrate()
66
91
 
@@ -75,12 +100,14 @@ class SQLiteDB(AbstractDB):
75
100
  "Install the system library (e.g. 'apt install libsqlcipher-dev') "
76
101
  "then: pip install hivemind-sqlite-database[cipher]"
77
102
  )
78
- conn = _sqlcipher.connect(self._db_path, check_same_thread=False)
103
+ conn = _sqlcipher.connect(self._db_path, check_same_thread=False,
104
+ uri=self._is_uri)
79
105
  conn.row_factory = _sqlcipher.Row
80
106
  escaped_password = self.password.replace("'", "''")
81
107
  conn.execute(f"PRAGMA key='{escaped_password}'")
82
108
  else:
83
- conn = sqlite3.connect(self._db_path, check_same_thread=False)
109
+ conn = sqlite3.connect(self._db_path, check_same_thread=False,
110
+ uri=self._is_uri)
84
111
  conn.row_factory = sqlite3.Row
85
112
  conn.execute(f"PRAGMA busy_timeout={int(self.BUSY_TIMEOUT_MS)}")
86
113
  return conn
@@ -102,10 +129,15 @@ class SQLiteDB(AbstractDB):
102
129
 
103
130
  @conn.setter
104
131
  def conn(self, value) -> None:
105
- """Adopt an already-open connection for the calling thread.
106
-
107
- Only the calling thread sees it; other threads still open their
108
- own. Tests use this to inject an in-memory database.
132
+ """Adopt an already-open connection for the calling thread only.
133
+
134
+ Any other thread that later touches ``.conn`` still opens its own
135
+ connection against ``self._db_path`` if that is the real client
136
+ database, that thread silently writes to it. Pass ``db_path``
137
+ (e.g. ``":memory:"``) to the constructor instead so every thread,
138
+ present and future, agrees on the same target; this setter exists
139
+ only for single-threaded call sites that already hold a connection
140
+ they want reused.
109
141
  """
110
142
  self._thread_state().conn = value
111
143
 
@@ -1,8 +1,8 @@
1
1
  # START_VERSION_BLOCK
2
2
  VERSION_MAJOR = 0
3
3
  VERSION_MINOR = 4
4
- VERSION_BUILD = 2
5
- VERSION_ALPHA = 1
4
+ VERSION_BUILD = 3
5
+ VERSION_ALPHA = 2
6
6
  # END_VERSION_BLOCK
7
7
 
8
8
  __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hivemind-sqlite-database
3
- Version: 0.4.2a1
3
+ Version: 0.4.3a2
4
4
  Summary: sqlite database plugin for hivemind-core
5
5
  Author-email: jarbasAi <jarbasai@mailfence.com>
6
6
  License: Apache-2.0
@@ -422,6 +422,56 @@ class TestSQLiteDBRoundTrip(unittest.TestCase):
422
422
  self.assertEqual(r.intent_blacklist, ["a:b"])
423
423
 
424
424
 
425
+ class TestSQLiteDBPathOverride(unittest.TestCase):
426
+ """A worker thread must never fall back to the real client database
427
+ just because the test only overrode `.conn` on the main thread."""
428
+
429
+ def test_db_path_override_keeps_worker_threads_off_disk(self):
430
+ import unittest.mock as mock
431
+ with tempfile.TemporaryDirectory() as tmpdir:
432
+ with mock.patch(
433
+ "hivemind_sqlite_database.xdg_data_home", return_value=tmpdir
434
+ ):
435
+ db = SQLiteDB(name="clients", subfolder="hivemind-core",
436
+ db_path=":memory:")
437
+
438
+ # Read a row written on this thread. "SELECT 1" would answer
439
+ # the same against a private, empty database and so would
440
+ # never notice a per-thread database.
441
+ db.add_item(Client(client_id=1, api_key="key",
442
+ name="kitchen"))
443
+
444
+ errors = []
445
+ seen = []
446
+
447
+ def worker():
448
+ try:
449
+ seen.extend(db.search_by_value("name", "kitchen"))
450
+ except Exception as e: # noqa: BLE001
451
+ errors.append(e)
452
+
453
+ t = threading.Thread(target=worker)
454
+ t.start()
455
+ t.join()
456
+
457
+ self.assertEqual(errors, [])
458
+ self.assertEqual([c.client_id for c in seen], [1])
459
+ real_db_file = os.path.join(tmpdir, "hivemind-core", "clients.db")
460
+ self.assertFalse(os.path.exists(real_db_file))
461
+
462
+ def test_two_in_memory_databases_stay_independent(self):
463
+ first = SQLiteDB(db_path=":memory:")
464
+ second = SQLiteDB(db_path=":memory:")
465
+ first.add_item(Client(client_id=1, api_key="key", name="kitchen"))
466
+ self.assertEqual(second.search_by_value("name", "kitchen"), [])
467
+
468
+ def test_explicit_db_path_creates_missing_directories(self):
469
+ with tempfile.TemporaryDirectory() as tmpdir:
470
+ path = os.path.join(tmpdir, "newdir", "clients.db")
471
+ SQLiteDB(db_path=path)
472
+ self.assertTrue(os.path.isfile(path))
473
+
474
+
425
475
  class TestSQLiteDBCommit(unittest.TestCase):
426
476
  def test_commit_returns_true(self):
427
477
  db = make_db()