Class: Whodunit::Chronicles::Ledgers::SQLiteLedger
- Inherits:
-
Whodunit::Chronicles::Ledger
- Object
- Whodunit::Chronicles::Ledger
- Whodunit::Chronicles::Ledgers::SQLiteLedger
- Defined in:
- lib/whodunit/chronicles/ledgers/sqlite_ledger.rb,
sig/whodunit/chronicles/ledgers/sqlite_ledger.rbs
Overview
SQLite-backed embedded durable ledger.
SQLiteLedger is the default solid local book. It can create its table, create indexes, report status, and append immutable ledger entries. The sqlite3 gem is loaded lazily only when a connection is not injected.
Constant Summary collapse
- DEFAULT_TABLE =
Default SQLite table for entries.
'whodunit_chronicles_entries'
Instance Attribute Summary collapse
-
#path ⇒ String
readonly
Path to the SQLite database file.
-
#table_name ⇒ String
readonly
Table receiving entries.
Instance Method Summary collapse
-
#append(entry) ⇒ LedgerEntry
Append one ledger entry.
-
#bind_values(entry) ⇒ Array[untyped]
Build bind values for one entry.
-
#connection ⇒ Object
Return the SQLite connection, creating it lazily when required.
-
#count_entries ⇒ Integer
Count persisted entries.
-
#ensure_indexes! ⇒ SQLiteLedger
Create indexes useful for audit lookup and de-duplication.
-
#index_name(suffix) ⇒ String
Build a safe index name from the table and suffix.
-
#initialize(path:, table_name: DEFAULT_TABLE, connection: nil) ⇒ SQLiteLedger
constructor
Create a SQLite-backed ledger.
-
#prepare! ⇒ SQLiteLedger
Create the entries table if needed.
-
#prepared? ⇒ Boolean
Determine whether the entries table exists.
-
#quote_identifier(identifier) ⇒ String
Quote a SQLite identifier.
-
#quoted_table_name ⇒ String
Quote the configured table name.
-
#serialize_time(value) ⇒ String
Serialize time-like objects consistently.
-
#sqlite_constraint_error?(error) ⇒ Boolean
Return whether an error is SQLite's unique constraint failure.
-
#status ⇒ Hash<Symbol, Object>
Return lightweight operational status for this ledger.
Methods inherited from Whodunit::Chronicles::Ledger
#migrate!, #partition_for, #verify
Constructor Details
#initialize(path:, table_name: DEFAULT_TABLE, connection: nil) ⇒ SQLiteLedger
Create a SQLite-backed ledger.
31 32 33 34 35 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 31 def initialize(path:, table_name: DEFAULT_TABLE, connection: nil) @path = path.to_s @table_name = table_name.to_s @connection = connection end |
Instance Attribute Details
#path ⇒ String (readonly)
Returns path to the SQLite database file.
21 22 23 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 21 def path @path end |
#table_name ⇒ String (readonly)
Returns table receiving entries.
24 25 26 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 24 def table_name @table_name end |
Instance Method Details
#append(entry) ⇒ LedgerEntry
Append one ledger entry.
75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 75 def append(entry) connection.execute(<<~SQL, bind_values(entry)) INSERT INTO #{quoted_table_name} ( event_id, occurred_at, recorded_at, namespace, entity, identity, operation, actor, changes, metadata, payload ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) SQL entry rescue StandardError => e raise unless sqlite_constraint_error?(e) raise AppendError, "duplicate ledger event_id: #{entry.event_id}" end |
#bind_values(entry) ⇒ Array[untyped]
Build bind values for one entry.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 113 def bind_values(entry) [ entry.event_id, serialize_time(entry.occurred_at), serialize_time(entry.recorded_at), entry.namespace, entry.entity, JSON.generate(entry.identity), entry.operation.to_s, JSON.generate(entry.actor), JSON.generate(entry.changes), JSON.generate(entry.), JSON.generate(entry.payload) ] end |
#connection ⇒ Object
Return the SQLite connection, creating it lazily when required.
105 106 107 108 109 110 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 105 def connection @connection ||= begin require 'sqlite3' SQLite3::Database.new(path) end end |
#count_entries ⇒ Integer
Count persisted entries.
146 147 148 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 146 def count_entries connection.execute("SELECT COUNT(*) FROM #{quoted_table_name}").first.first end |
#ensure_indexes! ⇒ SQLiteLedger
Create indexes useful for audit lookup and de-duplication.
63 64 65 66 67 68 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 63 def ensure_indexes! connection.execute("CREATE UNIQUE INDEX IF NOT EXISTS #{index_name(:event_id)} ON #{quoted_table_name} (event_id)") connection.execute("CREATE INDEX IF NOT EXISTS #{index_name(:entity)} ON #{quoted_table_name} (namespace, entity)") connection.execute("CREATE INDEX IF NOT EXISTS #{index_name(:occurred_at)} ON #{quoted_table_name} (occurred_at)") self end |
#index_name(suffix) ⇒ String
Build a safe index name from the table and suffix.
166 167 168 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 166 def index_name(suffix) quote_identifier("#{table_name.gsub(/\W+/, '_')}_#{suffix}_idx") end |
#prepare! ⇒ SQLiteLedger
Create the entries table if needed.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 40 def prepare! connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS #{quoted_table_name} ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id TEXT NOT NULL, occurred_at TEXT NOT NULL, recorded_at TEXT NOT NULL, namespace TEXT, entity TEXT, identity TEXT, operation TEXT NOT NULL, actor TEXT, changes TEXT, metadata TEXT, payload TEXT NOT NULL ) SQL self end |
#prepared? ⇒ Boolean
Determine whether the entries table exists.
137 138 139 140 141 142 143 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 137 def prepared? rows = connection.execute( "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [table_name] ) !rows.empty? end |
#quote_identifier(identifier) ⇒ String
Quote a SQLite identifier.
156 157 158 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 156 def quote_identifier(identifier) %("#{identifier.to_s.gsub('"', '""')}") end |
#quoted_table_name ⇒ String
Quote the configured table name.
161 162 163 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 161 def quoted_table_name @quoted_table_name ||= table_name.split('.').map { |part| quote_identifier(part) }.join('.') end |
#serialize_time(value) ⇒ String
Serialize time-like objects consistently.
130 131 132 133 134 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 130 def serialize_time(value) return value.iso8601 if value.respond_to?(:iso8601) value.to_s end |
#sqlite_constraint_error?(error) ⇒ Boolean
Return whether an error is SQLite's unique constraint failure.
151 152 153 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 151 def sqlite_constraint_error?(error) !!(defined?(SQLite3::ConstraintException) && error.is_a?(SQLite3::ConstraintException)) end |
#status ⇒ Hash<Symbol, Object>
Return lightweight operational status for this ledger.
92 93 94 95 96 97 98 99 100 |
# File 'lib/whodunit/chronicles/ledgers/sqlite_ledger.rb', line 92 def status { adapter: 'sqlite', path: path, table_name: table_name, prepared: prepared?, entries: prepared? ? count_entries : nil } end |