Class: Tina4::Drivers::SqliteDriver
- Inherits:
-
Object
- Object
- Tina4::Drivers::SqliteDriver
- Includes:
- Tina4::DatabaseAdapter, SchemaSplit
- Defined in:
- lib/tina4/drivers/sqlite_driver.rb
Constant Summary
Constants included from Tina4::DatabaseAdapter
Tina4::DatabaseAdapter::CONNECT_TIMEOUT_SLACK_SECONDS, Tina4::DatabaseAdapter::CONNECT_TIMEOUT_VAR, Tina4::DatabaseAdapter::CONTRACT, Tina4::DatabaseAdapter::DEFAULT_CONNECT_TIMEOUT_SECONDS
Class Attribute Summary collapse
-
.write_lock ⇒ Object
readonly
Returns the value of attribute write_lock.
Instance Attribute Summary collapse
-
#connection ⇒ Object
readonly
Returns the value of attribute connection.
Class Method Summary collapse
-
.resolve_path(connection_string) ⇒ Object
Resolve a SQLite URL / path against the project root (cwd).
Instance Method Summary collapse
-
#affected_rows ⇒ Object
Rows changed by the most recent INSERT/UPDATE/DELETE on this connection.
-
#apply_limit(sql, limit, offset = 0) ⇒ Object
The clause goes on a NEW LINE.
- #begin_transaction ⇒ Object
- #close ⇒ Object
-
#coerce_params(params) ⇒ Object
Coerce Ruby values to types the sqlite3 gem can bind.
- #columns(table_name) ⇒ Object
-
#commit ⇒ Object
Committing/rolling back when no transaction is open is a harmless no-op, NOT a failure — SQLite raises "cannot commit - no transaction is active" in that case.
-
#connect(connection_string, username: nil, password: nil) ⇒ Object
NOT bounded by TINA4_DATABASE_CONNECT_TIMEOUT, deliberately: this connect opens a LOCAL FILE.
- #execute(sql, params = []) ⇒ Object
- #execute_query(sql, params = []) ⇒ Object
- #last_insert_id ⇒ Object
- #placeholder ⇒ Object
- #placeholders(count) ⇒ Object
- #rollback ⇒ Object
-
#table_exists?(name) ⇒ Boolean
v3.13.14 (#48): a SQLite "schema" is an ATTACH alias ("extra.widget").
- #tables ⇒ Object
Methods included from SchemaSplit
Methods included from Tina4::DatabaseAdapter
bounding_connect, connect_timed_out!, connect_timeout_seconds, connect_timeout_whole_seconds, implemented_by?
Class Attribute Details
.write_lock ⇒ Object (readonly)
Returns the value of attribute write_lock.
21 22 23 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 21 def write_lock @write_lock end |
Instance Attribute Details
#connection ⇒ Object (readonly)
Returns the value of attribute connection.
10 11 12 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 10 def connection @connection end |
Class Method Details
.resolve_path(connection_string) ⇒ Object
Resolve a SQLite URL / path against the project root (cwd).
Convention (matches tina4-python, tina4-php, tina4-nodejs):
sqlite::memory: → :memory:
sqlite:///:memory: → :memory:
sqlite:///app.db → {cwd}/app.db (relative)
sqlite:///data/app.db → {cwd}/data/app.db (relative; auto-mkdir under cwd)
sqlite:////var/data/app.db → /var/data/app.db (absolute; no auto-mkdir)
sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
Never mkdir outside cwd — that was the root cause of the "Read-only file system: '/data'" crash on macOS.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 51 def self.resolve_path(connection_string) return ":memory:" if connection_string == "sqlite::memory:" || connection_string == "sqlite:///:memory:" # Strip the scheme + up to three slashes, preserving a potential fourth # slash (absolute) or drive letter. # `sqlite3:` is a documented alias for `sqlite:`. Normalise it FIRST or # none of the strips below match and `raw` keeps the whole connection # string, so the database file is literally named "sqlite3:app.db". # Not merely ugly: a colon is an illegal filename character on Windows, # so the documented alias is unusable there. DatabaseUrl already # normalises it; this method duplicates the strip instead of calling # it, which is how the two drifted. normalised = connection_string.sub(/^sqlite3:/, "sqlite:") raw = normalised.sub(/^sqlite:\/\/\//, "").sub(/^sqlite:\/\//, "").sub(/^sqlite:/, "") return ":memory:" if raw == ":memory:" is_windows_abs = raw.match?(/^[A-Za-z]:[\/\\]/) is_unix_abs = raw.start_with?("/") if is_windows_abs || is_unix_abs # Absolute — trust the user; don't auto-mkdir outside cwd. raw else # Relative — resolve under cwd; auto-mkdir parent dir. resolved = File.join(Dir.pwd, raw) parent = File.dirname(resolved) require "fileutils" FileUtils.mkdir_p(parent) unless File.directory?(parent) resolved end end |
Instance Method Details
#affected_rows ⇒ Object
Rows changed by the most recent INSERT/UPDATE/DELETE on this connection. Feeds Database#insert/update/delete's DatabaseResult.affected_rows (parity with the Python master + PHP, which surface affectedRows on writes).
121 122 123 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 121 def affected_rows @connection.changes end |
#apply_limit(sql, limit, offset = 0) ⇒ Object
The clause goes on a NEW LINE. Appended inline it lands INSIDE a trailing
-- comment and is silently swallowed by the engine — MEASURED:
"SELECT * FROM t ORDER BY id -- LIMIT 5" returned all 150 rows with the
100-row cap in force. That is a bug at the APPEND SITE, independent of
the detector (Database.has_trailing_limit?), and it needs its own test.
138 139 140 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 138 def apply_limit(sql, limit, offset = 0) "#{sql}\nLIMIT #{limit} OFFSET #{offset}" end |
#begin_transaction ⇒ Object
142 143 144 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 142 def begin_transaction @connection.execute("BEGIN TRANSACTION") end |
#close ⇒ Object
83 84 85 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 83 def close @connection&.close end |
#coerce_params(params) ⇒ Object
Coerce Ruby values to types the sqlite3 gem can bind. The gem RAISES ("can't prepare TrueClass") on a raw boolean, so map true/false to 1/0 — SQLite stores booleans as INTEGER 0/1. Time/DateTime serialise to ISO-8601 so a datetime field round-trips. Parity with the Python/PHP/Node adapters, which coerce booleans at the same bind boundary.
101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 101 def coerce_params(params) return params unless params.is_a?(Array) params.map do |value| case value when true then 1 when false then 0 when Time, DateTime then value.respond_to?(:iso8601) ? value.iso8601 : value.to_s else value end end end |
#columns(table_name) ⇒ Object
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 180 def columns(table_name) # v3.13.14 (#48): PRAGMA accepts an attached-schema prefix. schema, tbl = split_schema(table_name) pragma = schema && identifier?(schema) && identifier?(tbl) ? "#{schema}.table_info(#{tbl})" : "table_info(#{table_name})" rows = execute_query("PRAGMA #{pragma}") rows.map do |r| { name: r[:name], type: r[:type], nullable: r[:notnull] == 0, default: r[:dflt_value], # PRAGMA table_info reports `pk` as the 1-BASED POSITION within the # primary key, not a boolean: a composite key gives pk=1, pk=2, ... # Testing `== 1` reported only the first column of a composite key. primary_key: r[:pk].to_i.positive? } end end |
#commit ⇒ Object
Committing/rolling back when no transaction is open is a harmless no-op, NOT a failure — SQLite raises "cannot commit - no transaction is active" in that case. Swallow ONLY that specific condition so a stray commit (e.g. after an autocommit standalone write) doesn't poison the Database-level @last_error. A genuine commit/rollback failure (disk I/O, constraint deferral, locked DB) still propagates so Database#commit can FAIL LOUD per the DB-contract.
153 154 155 156 157 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 153 def commit @connection.execute("COMMIT") rescue SQLite3::SQLException => e raise unless e..to_s.downcase.include?("no transaction is active") end |
#connect(connection_string, username: nil, password: nil) ⇒ Object
NOT bounded by TINA4_DATABASE_CONNECT_TIMEOUT, deliberately: this connect opens a LOCAL FILE. There is no network peer, so there is no host and no port for the contract's error message to name, and no handshake that can hang. Every other driver's bound lives in its own connect; this is the one that has nothing to bind.
29 30 31 32 33 34 35 36 37 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 29 def connect(connection_string, username: nil, password: nil) require "sqlite3" db_path = self.class.resolve_path(connection_string) @connection = SQLite3::Database.new(db_path) @connection.results_as_hash = true @connection.execute("PRAGMA journal_mode=WAL") @connection.execute("PRAGMA foreign_keys=ON") end |
#execute(sql, params = []) ⇒ Object
92 93 94 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 92 def execute(sql, params = []) @connection.execute(sql, coerce_params(params)) end |
#execute_query(sql, params = []) ⇒ Object
87 88 89 90 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 87 def execute_query(sql, params = []) results = @connection.execute(sql, coerce_params(params)) symbolize_rows(results) end |
#last_insert_id ⇒ Object
114 115 116 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 114 def last_insert_id @connection.last_insert_row_id end |
#placeholder ⇒ Object
125 126 127 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 125 def placeholder "?" end |
#placeholders(count) ⇒ Object
129 130 131 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 129 def placeholders(count) (["?"] * count).join(", ") end |
#rollback ⇒ Object
159 160 161 162 163 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 159 def rollback @connection.execute("ROLLBACK") rescue SQLite3::SQLException => e raise unless e..to_s.downcase.include?("no transaction is active") end |
#table_exists?(name) ⇒ Boolean
v3.13.14 (#48): a SQLite "schema" is an ATTACH alias ("extra.widget"). Query that database's own sqlite_master when the prefix is a plain identifier; otherwise treat the whole string as a bare table name.
168 169 170 171 172 173 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 168 def table_exists?(name) schema, tbl = split_schema(name) master = schema && identifier?(schema) ? "#{schema}.sqlite_master" : "sqlite_master" rows = execute_query("SELECT 1 FROM #{master} WHERE type='table' AND name=?", [tbl]) !rows.empty? end |
#tables ⇒ Object
175 176 177 178 |
# File 'lib/tina4/drivers/sqlite_driver.rb', line 175 def tables rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'") rows.map { |r| r[:name] } end |