Class: Rubino::Database::Connection
- Inherits:
-
Object
- Object
- Rubino::Database::Connection
- Defined in:
- lib/rubino/database/connection.rb
Overview
Manages the SQLite database connection via Sequel. Handles connection creation, WAL mode setup, and provides access to the underlying Sequel::Database instance.
Constant Summary collapse
- MEMORY_PATHS =
SQLite path values that resolve to an ephemeral, in-memory database rather than an on-disk file. These must skip File.expand_path (which would turn “:memory:” into a literal “./:memory:” file) and FileUtils.mkdir_p on the parent directory.
[":memory:", "file::memory:"].freeze
- BUSY_TIMEOUT_MS =
How long SQLite waits on a held lock before raising SQLite3::BusyException, in milliseconds. Set at OPEN time (Sequel’s :timeout option) so it covers the very first statements — the ‘PRAGMA journal_mode=WAL` write itself — not just queries that run after the explicit `PRAGMA busy_timeout`. A concurrent first-boot serializes its migration under a file lock (#race / #440), and a losing racer that opens during the winner’s migration must WAIT the write lock out here rather than surface a raw ‘SQLite3::BusyException: database is locked` backtrace (#333/#359).
5_000- CONNECT_RETRY_BUDGET =
Bounded wall-clock budget (seconds) for the open + WAL-setup retry backstop. If ‘:timeout` is somehow not honoured on the very first write pragma (driver/filesystem quirks), we still wait a concurrent migration out instead of leaking a backtrace, then give up cleanly.
10.0
Instance Attribute Summary collapse
-
#db_path ⇒ Object
readonly
Returns the value of attribute db_path.
Instance Method Summary collapse
-
#close ⇒ Object
Closes the database connection.
-
#corrupt? ⇒ Boolean
True when the on-disk file is present but unopenable because its image is malformed/truncated (‘SQLite3::CorruptException`).
-
#corruption_error?(error) ⇒ Boolean
True when
error(or anything in its cause chain) is a SQLite corruption/garbage-header error. -
#db ⇒ Object
Returns the Sequel database connection (lazy-initialized).
-
#healthy? ⇒ Boolean
Tests if the database is accessible.
-
#initialize(db_path) ⇒ Connection
constructor
A new instance of Connection.
-
#memory? ⇒ Boolean
True when @db_path refers to an in-memory SQLite instance.
-
#quarantine! ⇒ Object
Quarantine an unopenable database file (and its WAL/SHM siblings) by renaming them aside to ‘<name>.corrupt-<timestamp>` so a fresh DB can be created in their place WITHOUT silently destroying the bytes — the user can still hand them to `sqlite3 .recover` if they want.
Constructor Details
#initialize(db_path) ⇒ Connection
Returns a new instance of Connection.
51 52 53 |
# File 'lib/rubino/database/connection.rb', line 51 def initialize(db_path) @db_path = memory_path?(db_path) ? db_path : File.(db_path) end |
Instance Attribute Details
#db_path ⇒ Object (readonly)
Returns the value of attribute db_path.
49 50 51 |
# File 'lib/rubino/database/connection.rb', line 49 def db_path @db_path end |
Instance Method Details
#close ⇒ Object
Closes the database connection
134 135 136 137 |
# File 'lib/rubino/database/connection.rb', line 134 def close @db&.disconnect @db = nil end |
#corrupt? ⇒ Boolean
True when the on-disk file is present but unopenable because its image is malformed/truncated (‘SQLite3::CorruptException`). A brand-new or absent file is NOT corrupt — it’s just not initialized yet — so this is the signal that distinguishes “needs setup” from “needs recovery”.
72 73 74 75 76 77 78 79 |
# File 'lib/rubino/database/connection.rb', line 72 def corrupt? return false if memory? || !File.exist?(@db_path) db.execute("SELECT 1") false rescue StandardError => e corruption_error?(e) end |
#corruption_error?(error) ⇒ Boolean
True when error (or anything in its cause chain) is a SQLite corruption/garbage-header error. Two distinct driver exceptions signal a corrupt-but-present file:
* SQLite3::CorruptException / "database disk image is malformed" — a
valid SQLite header with internal damage.
* SQLite3::NotADatabaseException / "file is not a database" (#377) — a
garbage or truncated header, so SQLite can't even recognise it as a
DB. This is just as much "corrupt-but-present" as the malformed case:
the file exists and isn't openable, so it must route to the doctor /
setup-quarantine path, NOT be reported as "not set up", and user
commands (sessions list/compact) must not leak a raw backtrace.
Sequel wraps the driver exception in a Sequel::DatabaseError, so we walk #cause and also match the wrapped class name + message substrings without hard-depending on the sqlite3 gem constants being loaded.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/rubino/database/connection.rb', line 117 def corruption_error?(error) e = error while e name = e.class.name.to_s return true if name.include?("SQLite3::CorruptException") return true if name.include?("SQLite3::NotADatabaseException") msg = e..to_s return true if msg.include?("database disk image is malformed") return true if msg.include?("file is not a database") e = e.cause end false end |
#db ⇒ Object
Returns the Sequel database connection (lazy-initialized)
56 57 58 |
# File 'lib/rubino/database/connection.rb', line 56 def db @db ||= connect! end |
#healthy? ⇒ Boolean
Tests if the database is accessible
61 62 63 64 65 66 |
# File 'lib/rubino/database/connection.rb', line 61 def healthy? db.execute("SELECT 1") true rescue StandardError false end |
#memory? ⇒ Boolean
True when @db_path refers to an in-memory SQLite instance.
140 141 142 |
# File 'lib/rubino/database/connection.rb', line 140 def memory? memory_path?(@db_path) end |
#quarantine! ⇒ Object
Quarantine an unopenable database file (and its WAL/SHM siblings) by renaming them aside to ‘<name>.corrupt-<timestamp>` so a fresh DB can be created in their place WITHOUT silently destroying the bytes — the user can still hand them to `sqlite3 .recover` if they want. Returns the path the main file was moved to, or nil when there was nothing to move.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/rubino/database/connection.rb', line 86 def quarantine! return nil if memory? || !File.exist?(@db_path) close stamp = Time.now.strftime("%Y%m%d%H%M%S") moved = nil ["", "-wal", "-shm"].each do |suffix| src = "#{@db_path}#{suffix}" next unless File.exist?(src) dest = "#{@db_path}.corrupt-#{stamp}#{suffix}" File.rename(src, dest) moved = dest if suffix.empty? end moved end |