Class: Rubino::Database::Connection

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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.expand_path(db_path)
end

Instance Attribute Details

#db_pathObject (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

#closeObject

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”.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.message.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

#dbObject

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

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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