Class: Sqlx::Database::DatabaseConnection

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

Mutex.new

Instance Method Summary collapse

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.

Parameters:

  • rc (Integer) —

    the driver's result code

Returns:

  • (Boolean) —

    true for SQLITE_BUSY and SQLITE_LOCKED



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

Parameters:

  • sql (String) —

    the composed SQL to prepare

Returns:

  • (Object) —

    the statement's driver handle



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.

Parameters:

  • sql (String) (defaults to: nil) —

    optional statement text, only to enrich the error message

Returns:

  • (Boolean) —

    true when a row is ready, false when the statement finished

Raises:

  • (Sqlx::RecordNotUnique) —

    a UNIQUE constraint rejects the statement; any other driver error raises RuntimeError naming the statement



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.

Parameters:

  • message (String) —

    the driver's error message

Returns:

  • (Boolean) —

    true when the message reports a UNIQUE violation



292
293
294
# File 'runtime/database/sqlite.rb', line 292

def unique_violation?(message)
  message.include?("UNIQUE constraint failed")
end