Class: Insika::Stores::SQLite
- Inherits:
-
Object
- Object
- Insika::Stores::SQLite
- Includes:
- Insika::Store
- Defined in:
- lib/insika/stores/sqlite.rb
Overview
SQLite backend — the production default. A single kv table; the domain lives in the scopes. One handle per process, writes in a transaction serialized by an Async::Semaphore.
Constant Summary collapse
- DDL =
<<~SQL CREATE TABLE IF NOT EXISTS kv ( scope TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (scope, key) ) WITHOUT ROWID; SQL
Instance Method Summary collapse
- #close ⇒ Object
- #delete(scope, key) ⇒ Object
- #get(scope, key) ⇒ Object
-
#initialize(path:, serializer: JSON) ⇒ SQLite
constructor
lazy require: the core installs without the sqlite3 gem when only Memory is used.
- #list(scope, prefix = nil) ⇒ Object
- #set(scope, key, value) ⇒ Object
-
#transaction(&blk) ⇒ Object
BEGIN IMMEDIATE ...
Constructor Details
#initialize(path:, serializer: JSON) ⇒ SQLite
lazy require: the core installs without the sqlite3 gem when only Memory is used.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/insika/stores/sqlite.rb', line 35 def initialize(path:, serializer: JSON) require "sqlite3" @serializer = serializer @db = SQLite3::Database.new(path) @write_semaphore = Async::Semaphore.new(1) @tx_owner = nil # Multi-process boot (N Falcon workers opening the SAME file at the # same time): `PRAGMA journal_mode = WAL` on a new file needs an # EXCLUSIVE lock and may return SQLITE_BUSY right then — the busy # timeout alone does NOT cover the journal-mode switch. Hence: timeout # FIRST (covers the DDL and the hot path) + retry with backoff around # the initialization (covers the WAL-switch race). Idempotent: # reopening already-in-WAL is a no-op. # # `busy_handler_timeout=`, NOT `busy_timeout=`: the C-level handler # sleeps holding the GVL, so a worker waiting on another PROCESS's # write lock would stall every fiber it is running for up to the full # timeout (measured: a waiter blocks the winner's own commit). The # Ruby-level handler sleeps in Ruby — the scheduler keeps the rest of # the worker breathing while this handle waits its turn. @db.busy_handler_timeout = 5_000 with_busy_retry do @db.execute("PRAGMA journal_mode = WAL") @db.execute("PRAGMA synchronous = NORMAL") @db.execute_batch(DDL) end # The WITHOUT ROWID PRIMARY KEY (scope, key) is already the prefix index — # there is no extra index to create. rescue ::SQLite3::Exception => e raise Insika::StoreError, "failed to open #{path}: #{e.}" end |
Instance Method Details
#close ⇒ Object
89 90 91 92 |
# File 'lib/insika/stores/sqlite.rb', line 89 def close @db&.close nil end |
#delete(scope, key) ⇒ Object
115 116 117 118 119 120 |
# File 'lib/insika/stores/sqlite.rb', line 115 def delete(scope, key) transaction do @db.execute("DELETE FROM kv WHERE scope = ? AND key = ?", [scope, key]) @db.changes.positive? end end |
#get(scope, key) ⇒ Object
94 95 96 97 98 99 100 101 |
# File 'lib/insika/stores/sqlite.rb', line 94 def get(scope, key) row = @db.get_first_value( "SELECT value FROM kv WHERE scope = ? AND key = ?", [scope, key] ) row.nil? ? nil : @serializer.parse(row) rescue ::SQLite3::Exception => e raise Insika::StoreError, e. end |
#list(scope, prefix = nil) ⇒ Object
122 123 124 125 126 127 128 129 |
# File 'lib/insika/stores/sqlite.rb', line 122 def list(scope, prefix = nil) keys = @db.execute( "SELECT key FROM kv WHERE scope = ? ORDER BY key", [scope] ).map(&:first) prefix ? keys.select { |k| k.start_with?(prefix) } : keys rescue ::SQLite3::Exception => e raise Insika::StoreError, e. end |
#set(scope, key, value) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/insika/stores/sqlite.rb', line 103 def set(scope, key, value) serialized = serialize(value) # fail-fast BEFORE writing transaction do @db.execute( "INSERT OR REPLACE INTO kv (scope, key, value, updated_at) " \ "VALUES (?, ?, ?, ?)", [scope, key, serialized, Time.now.utc.iso8601] ) end value end |
#transaction(&blk) ⇒ Object
BEGIN IMMEDIATE ... COMMIT/ROLLBACK, serialized by the semaphore. A nested one reuses the outer transaction.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/insika/stores/sqlite.rb', line 133 def transaction(&blk) return yield if @tx_owner == Fiber.current @write_semaphore.acquire do begin @db.transaction(:immediate) @tx_owner = Fiber.current result = yield @db.commit result rescue StandardError @db.rollback if @db.transaction_active? raise ensure @tx_owner = nil end end rescue ::SQLite3::Exception => e raise Insika::StoreError, e. end |