Module: Sqlx::Database

Defined in:
runtime/database/query_cache.rb,
runtime/database/lease.rb,
runtime/database/postgresql.rb,
runtime/database/sqlite.rb

Overview

The cache's module surface, part of the adapter contract both backends satisfy. The adapters call these from exec_read (fetch/store), exec / exec_write / configure (invalidate), and with_connection (scope lifetime).

Defined Under Namespace

Modules: Libpq, SQLite3 Classes: DatabaseConnection, DatabasePool, LeaseToken, PgConnection, PgPool, PgStmt, Stmt

Class Method Summary collapse

Class Method Details

.adapter_name ⇒ String

the dialect name apps branch on; each adapter answers its own.

Returns:

  • (String) —

    this adapter's dialect name, "SQLite"



706
707
708
# File 'runtime/database/postgresql.rb', line 706

def self.adapter_name
  "PostgreSQL"
end

.changes ⇒ Integer

the row count of the last non-RETURNING INSERT/UPDATE/DELETE on this lease's connection. A statement with RETURNING reports its count through exec_write, not here; this accessor keeps the previous count. SQLite's sqlite3_changes does update for RETURNING — core never reads it after a RETURNING write.

Returns:

  • (Integer) —

    the affected-row count of the last row-changing statement without RETURNING

Raises:



689
690
691
# File 'runtime/database/postgresql.rb', line 689

def self.changes
  current_conn.changes
end

.close ⇒ Object



511
512
513
514
515
516
517
518
# File 'runtime/database/postgresql.rb', line 511

def self.close
  if !@pool.nil?
    @pool.close_all
    @pool = nil
  end
  # Cached rows belong to the connections that produced them.
  query_cache_clear
end

.configure(path, pool_size: 8, busy_timeout_ms: 5000) ⇒ void

configure the database and build its connection pools: pool_size FFI handles to path, opened on first use. Nothing is opened here, so a prefork parent may call this before fork; the parent must not touch the database before forking, or the child would inherit the parent's handles.

Every connection gets the fair busy handler (busy_wait) with the same timeout. DATABASE_BUSY_TIMEOUT_MS overrides busy_timeout_ms when set.

Parameters:

  • path (String) —

    the database file to open (":memory:" for tests)

  • pool_size (Integer) (defaults to: 8) —

    requested handle count; DATABASE_POOL_SIZE overrides it when set

  • busy_timeout_ms (Integer) (defaults to: 5000) —

    how long one lock wait may take; 0 keeps SQLite's immediate Sqlx::Busy answer



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'runtime/database/postgresql.rb', line 481

def self.configure(conninfo, pool_size: 8)
  @sql_trace = ENV.fetch("RH_SQL_TRACE", "") != ""
  n = pool_size
  ev = ENV["DATABASE_POOL_SIZE"]
  if !ev.nil? && ev != ""
    n = ev.to_i
  end
  probe = PgConnection.new(conninfo)
  probe.close
  close unless @pool.nil?
  @pool = PgPool.new(conninfo, n)
  # A reconnect replaces the database behind any cached rows; invalidate
  # every request's cache before the new connection serves a read.
  query_cache_clear
end

.configure_default! ⇒ void

configure from this adapter's own source, so main.rb never names an adapter: DATABASE_PATH, else storage/development.sqlite3.



503
504
505
506
507
508
509
# File 'runtime/database/postgresql.rb', line 503

def self.configure_default!
  conninfo = ENV["PG_URL"]
  if conninfo.nil? || conninfo.empty?
    raise "PG_URL is required - this adapter speaks PostgreSQL (schema owned by the application)"
  end
  configure(conninfo)
end

.exec(sql) ⇒ void

run one DDL / INSERT / UPDATE / DELETE statement; no rows come back. Callers that want the inserted id or the row count read last_insert_rowid / changes immediately after.

Parameters:

  • sql (String) —

    the statement, with values inlined — this entry binds nothing

Raises:



532
533
534
535
536
537
538
539
540
541
542
543
# File 'runtime/database/postgresql.rb', line 532

def self.exec(sql)
  current_conn
  record_query(sql)
  started = Snails::Logger::Context.now_us
  begin
    res = pq_exec(autoincrement_shim(sql))
    Libpq.PQclear(res)
  ensure
    query_cache_clear
    Snails::Logger::Context.add_db_us(Snails::Logger::Context.now_us - started)
  end
end

.exec_read(sql, values) ⇒ Array

run one statement with the caller's values bound and hand back the rows it produced, keyed by column name. The binding is this adapter's (the same one exec_write uses); a caller that wants the rows of a statement with values comes here.

Inside a with_connection scope, an identical SELECT replays the first result. The cache key covers the statement and its bind values (cache_key_for), so bound reads are cached too — the same contract the PostgreSQL sibling answers. A write or DDL invalidates the cache; outside a scope every call round-trips.

Examples:

Sqlx::Database.exec_read("SELECT id FROM t WHERE a = ?", [ "x" ])
# => [ { "id" => 7 } ]

Parameters:

  • sql (String) —

    the statement to run, with a ? for each value

  • values (Array, nil) —

    one value per ? — nil for a statement that takes none. nil binds NULL, a Time binds in the runtime's stored form (UTC), everything else by its runtime type

Returns:

  • (Array) —

    one name-keyed row per row the statement produced — an empty array when it produced none

Raises:

  • (Sqlx::Busy) —

    the file's lock stayed held past the busy timeout



568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'runtime/database/postgresql.rb', line 568

def self.exec_read(sql, values)
  current_conn
  bound = values.nil? ? [] : values
  final = final_sql_for(sql, bound)
  cacheable = select_read?(sql)
  generation = @query_cache_generation
  if cacheable
    cached = query_cache_fetch(final, generation)
    unless cached.nil?
      record_cache_hit(sql)
      return cached
    end
  end
  record_query(sql)
  started = Snails::Logger::Context.now_us
  begin
    stmt = PgStmt.new(final)
    rows = read_rows(stmt)
    finalize(stmt)
    query_cache_store(final, rows, generation) if cacheable
    rows
  ensure
    Snails::Logger::Context.add_db_us(Snails::Logger::Context.now_us - started)
  end
end

.exec_write(sql, values) ⇒ Hash

run one statement with the caller's values bound and hand back what that execution produced: the rows it returned and the rows it changed.

The two answers belong to one execution — the count is final only once the statement is done, and a statement that returns rows is done after its last row is stepped — so one call carries both and the caller reads the one it needs.

Examples:

Sqlx::Database.exec_write("INSERT INTO t (a) VALUES (?)", [ "x" ])
# => { "rows" => [], "count" => 1 }
Sqlx::Database.exec_write("UPDATE t SET a = ? WHERE id = ? RETURNING id", [ "x", 7 ])
# => { "rows" => [ { "id" => 7 } ], "count" => 1 }

Parameters:

  • sql (String) —

    the statement to run, with ? placeholders

  • values (Array) —

    one value per ? — nil binds NULL, a Time binds in the runtime's stored form (UTC, whatever zone the caller handed over), everything else binds by its runtime type

Returns:

  • (Hash) —

    the execution's result, with two keys — "rows" holds one name-keyed row per row the statement RETURNED (an empty array when it returned none) and "count" holds the number of rows the statement CHANGED. The count is defined for row-changing statements (INSERT/UPDATE/DELETE): a statement that changes no rows reports 0, and a statement that returns rows reports the rows it returned (one per changed row), so count and row count agree. A statement that changes nothing and returns nothing (DDL) reports the driver's own last-change value, which is not a row count — DDL is not this entry's question.

Raises:

  • (Sqlx::RecordNotUnique) —

    a UNIQUE constraint rejects the statement

  • (Sqlx::Busy) —

    the file's lock stayed held past the busy timeout; the statement did not commit and may be retried as part of its unit



630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
# File 'runtime/database/postgresql.rb', line 630

def self.exec_write(sql, values)
  current_conn
  started = Snails::Logger::Context.now_us
  begin
    stmt = prepare(sql)
    i = 0
    while i < values.length
      bind_value(stmt, i + 1, values[i])
      i += 1
    end
    rows = read_rows(stmt)
    status = Libpq.PQresultStatus(stmt.res)
    count = 0
    if status == Libpq::PGRES_COMMAND_OK
      tuples = Libpq.PQcmdTuples(stmt.res)
      count = tuples.nil? || tuples == "" ? 0 : tuples.to_i
    else
      count = rows.length
    end
    finalize(stmt)
    { "rows" => rows, "count" => count }
  ensure
    query_cache_clear
    Snails::Logger::Context.add_db_us(Snails::Logger::Context.now_us - started)
  end
end

.in_lease? ⇒ Boolean

whether this thread is inside a with_connection scope. The scope is open even before the first database access; callers use it to avoid opening a nested scope that would rebind the outer one.

Returns:

  • (Boolean) —

    true when the thread is inside a lease scope



104
105
106
# File 'runtime/database/lease.rb', line 104

def self.in_lease?
  !Thread.current[:db_lease_scope].nil? || !Thread.current[:db_conn].nil?
end

.last_insert_rowid ⇒ Integer

the id the last INSERT in this session produced. PG identity columns advance a sequence rather than a rowid, so this reads lastval(): the session's last nextval(), the per-connection answer this adapter keeps. A session that has inserted nothing raises from PG; that reads back as 0, sqlite's answer too.

Returns:

  • (Integer) —

    the session's last sequence value, 0 before any INSERT

Raises:



675
676
677
678
# File 'runtime/database/postgresql.rb', line 675

def self.last_insert_rowid
  v = query_int("SELECT lastval()")
  v.nil? ? 0 : v
end

.owns_schema? ⇒ Boolean

whether this adapter owns its schema DDL. It answers true here; the PostgreSQL sibling answers false (there the schema belongs to the application's deployment). The server never runs DDL either way — an explicit loader (a harness or test setup step) reads this to decide whether to apply the app's schema.

Returns:

  • (Boolean) —

    true for this adapter



699
700
701
# File 'runtime/database/postgresql.rb', line 699

def self.owns_schema?
  false
end

.query_cache_clear ⇒ void

invalidate every thread's entries after a write or DDL. The generation bump is serialized: two writes landing together must not collapse into one value and leave a pre-write entry looking current.



167
168
169
170
171
172
173
# File 'runtime/database/query_cache.rb', line 167

def self.query_cache_clear
  @query_cache_lock.synchronize do
    @query_cache_generation = @query_cache_generation + 1
  end
  cache = Thread.current[:sqlx_query_cache]
  cache.clear_cached_rows unless cache.nil?
end

.quote(value) ⇒ String

the adapter's SQL literal renderer: NULL, a bare number, the adapter's boolean spelling, the stored Time form, else a quoted string. The escape helpers are the implementation; callers go through Sqlx.quote.

Returns:

  • (String) —

    the SQL literal for value



715
716
717
718
719
720
721
722
723
724
725
726
727
# File 'runtime/database/postgresql.rb', line 715

def self.quote(value)
  if value.nil?
    "NULL"
  elsif value.is_a?(Integer) || value.is_a?(Float)
    value.to_s
  elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
    escape_bool(value)
  elsif value.is_a?(Time)
    escape_string(value.to_fs(:db))
  else
    escape_string(value.to_s)
  end
end

.require_leases! ⇒ void

require a with_connection scope for every database access from here on. A server process (the scheduler, worker threads, and one thread per client connection) may otherwise reach a shared connection another thread holds mid-transaction. The fallback stays for single-fiber contexts (boot, tests, one-shot dev commands).



115
116
117
# File 'runtime/database/lease.rb', line 115

def self.require_leases!
  @lease_required = true
end

.sql_trace? ⇒ Boolean

Returns:

  • (Boolean)


729
730
731
# File 'runtime/database/postgresql.rb', line 729

def self.sql_trace?
  @sql_trace
end

.with_connection ⇒ Object

run the block inside a connection lease scope and hand back its value. The scope does not touch the pool by itself: the first database access checks a connection out (see ensure_connection), so a request, job, or log hand-off that never reaches the database never leases a connection. A thread already inside a scope re-enters it: the outer connection and request cache carry on, so a nested block can neither rebind nor unbind its outer scope. Before configure there is nothing to lease, so the block runs without a cache; a database access inside still fails loudly on the absent connection.

Returns:

  • (Object) —

    the block's value



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'runtime/database/lease.rb', line 55

def self.with_connection
  return yield if in_lease?

  Thread.current[:db_lease_scope] = true
  finished = false
  begin
    result = yield
    finished = true
  ensure
    Thread.current[:db_lease_scope] = nil
    conn = Thread.current[:db_conn]
    token = Thread.current[:db_lease_token]
    Thread.current[:db_lease_token] = nil
    if !conn.nil?
      query_cache_end
      Thread.current[:db_conn] = nil
      token.release(conn, finished) unless token.nil?
    end
  end
  result
end