Class: Sqlx::Database::DatabaseConnection
- Defined in:
- runtime/database/sqlite.rb
Overview
One pooled connection plus its prepared-statement cache. The cache is per-connection by necessity: a stmt carries its cursor and belongs to the connection that prepared it, so a shared map would let two fibers corrupt each other. The pool leases a connection to one fiber at a time, so the cache needs no mutex.
The key is the composed SQL: the static shape (WHERE id = ?) when the
bind gate is on, per-value when the lowerer inlines literals.
Constant Summary collapse
- CAP =
128- PREPARE_LOCK =
SQLite3.stmt_outis ONE 8-byte out-buffer for the whole process (anffi_buffer, static C storage), so two connections preparing at once would read each other's statement pointer and take the process down. The prepare and the read of that buffer are one critical section. Mutex.new
Instance Method Summary collapse
-
#busy_result?(rc) ⇒ Boolean
whether a driver result code means the lock could not be taken: another writer is committing, or a reader holds the file exclusively.
- #dbh ⇒ Object
-
#finalize_all ⇒ void
really finalize every cached statement, releasing its handle.
-
#initialize(dbh) ⇒ DatabaseConnection
constructor
A new instance of DatabaseConnection.
-
#prepare_cached(sql) ⇒ Object
the prepared stmt for
sql, from the cache on a hit and freshly prepared and cached on a miss. -
#step_real(stmt, sql = nil) ⇒ Boolean
advance one real statement by one row.
-
#trim! ⇒ void
bring the statement cache back to
CAP, finalizing what it drops. -
#unique_violation?(message) ⇒ Boolean
whether the driver's message reports a UNIQUE-index violation, so the raise sites can map it to
Sqlx::RecordNotUnique— the class apps write their rescues against, which keeps the raw driver error from reaching the dispatcher as a 500.
Constructor Details
#initialize(dbh) ⇒ DatabaseConnection
Returns a new instance of DatabaseConnection.
185 186 187 188 |
# File 'runtime/database/sqlite.rb', line 185 def initialize(dbh) @dbh = dbh @entries = [] end |
Instance Method Details
#busy_result?(rc) ⇒ Boolean
whether a driver result code means the lock could not be taken: another
writer is committing, or a reader holds the file exclusively. Both map
to Sqlx::Busy at every raise site, so a caller can retry a unit
instead of matching the "... is locked" message text.
303 304 305 |
# File 'runtime/database/sqlite.rb', line 303 def busy_result?(rc) rc == SQLite3::BUSY || rc == SQLite3::LOCKED end |
#dbh ⇒ Object
190 191 192 |
# File 'runtime/database/sqlite.rb', line 190 def dbh @dbh end |
#finalize_all ⇒ void
really finalize every cached statement, releasing its handle. Pool-shutdown path only.
311 312 313 314 315 316 317 |
# File 'runtime/database/sqlite.rb', line 311 def finalize_all i = 0 while i < @entries.length SQLite3.sqlite3_finalize(@entries[i].ptr) i += 1 end end |
#prepare_cached(sql) ⇒ Object
the prepared stmt for sql, from the cache on a hit and freshly
prepared and cached on a miss. The scan is linear: the hit rate matters,
not the lookup's constant.
EVERY prepared stmt goes into @entries, with no cap check — that is what
makes finalize's reset-don't-close correct for all of them, since every
stmt handed back stays reachable and finalizable at trim!.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'runtime/database/sqlite.rb', line 204 def prepare_cached(sql) i = 0 while i < @entries.length e = @entries[i] return e.ptr if e.sql == sql i += 1 end # `SQLite3.stmt_out` is ONE 8-byte out-buffer for the whole process (an # `ffi_buffer`, static C storage), so two connections preparing at once # would read each other's statement pointer and take the process down. # The prepare and the read of that buffer are one critical section. No # raise inside the lock: `synchronize` has no ensure on this adapter. rc = 0 st = 0 PREPARE_LOCK.synchronize do rc = SQLite3.sqlite3_prepare_v2(@dbh, sql, -1, SQLite3.stmt_out, nil) st = rc == SQLite3::OK ? SQLite3.read_ptr(SQLite3.stmt_out) : 0 end if rc != SQLite3::OK raise Sqlx::Busy, SQLite3.sqlite3_errmsg(@dbh) if busy_result?(rc) raise "Sqlx::Database.prepare failed (" + rc.to_s + "): " + SQLite3.sqlite3_errmsg(@dbh) + " — sql: " + sql end @entries.push(Stmt.new(sql, st)) st end |
#step_real(stmt, sql = nil) ⇒ Boolean
advance one real statement by one row. The connection owns the driver
call so the module's step? and the replay path share one
implementation.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'runtime/database/sqlite.rb', line 265 def step_real(stmt, sql = nil) rc = SQLite3.sqlite3_step(stmt) if rc == SQLite3::ROW return true elsif rc == SQLite3::DONE return false end msg = SQLite3.sqlite3_errmsg(@dbh) SQLite3.sqlite3_reset(stmt) SQLite3.sqlite3_clear_bindings(stmt) # The parameterized path is the standard write path, so a UNIQUE # violation maps to RecordNotUnique here the same way `exec` maps it. raise Sqlx::Busy, msg if busy_result?(rc) raise Sqlx::RecordNotUnique, msg if unique_violation?(msg) suffix = sql.nil? ? "" : " — sql: " + sql raise "Sqlx::Database.step failed (" + rc.to_s + "): " + msg + suffix end |
#trim! ⇒ void
bring the statement cache back to CAP, finalizing what it drops. Call
it at the request boundary (with_connection), never mid-request: a
stmt an open cursor still holds must not be closed, and between requests
there are none.
The CAP most-recent entries survive — the tail, since prepare_cached
appends, and recency is the best available proxy for reuse.
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'runtime/database/sqlite.rb', line 239 def trim! return nil if @entries.length <= CAP keep = [] drop_before = @entries.length - CAP i = 0 while i < @entries.length if i < drop_before SQLite3.sqlite3_finalize(@entries[i].ptr) else keep.push(@entries[i]) end i += 1 end @entries = keep end |
#unique_violation?(message) ⇒ Boolean
whether the driver's message reports a UNIQUE-index violation, so the
raise sites can map it to Sqlx::RecordNotUnique — the class apps write
their rescues against, which keeps the raw driver error from reaching
the dispatcher as a 500. The test is sqlite's own engine message rather
than a driver exception class, so one rule needs no per-driver class
table.
292 293 294 |
# File 'runtime/database/sqlite.rb', line 292 def unique_violation?() .include?("UNIQUE constraint failed") end |