From 545f3c050ff856f7cc7d1c372b041e62265dc336 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 11 May 2026 00:30:48 -0600 Subject: [PATCH 1/9] Add Hanami::DB::SQLite namespace and UnknownPragmaError Bootstraps the SQLite namespace and a dedicated exception class for the validation case: when an override names a pragma the linked SQLite doesn't recognise. Carries the offending names as structured data alongside the message and freezes its inputs so callers can hold references safely. The error message tells the user how to opt out (`validate: false`). --- lib/hanami/db/gem_inflector.rb | 1 + lib/hanami/db/sqlite.rb | 8 ++++ lib/hanami/db/sqlite/unknown_pragma_error.rb | 25 ++++++++++++ spec/unit/sqlite/unknown_pragma_error_spec.rb | 40 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 lib/hanami/db/sqlite.rb create mode 100644 lib/hanami/db/sqlite/unknown_pragma_error.rb create mode 100644 spec/unit/sqlite/unknown_pragma_error_spec.rb diff --git a/lib/hanami/db/gem_inflector.rb b/lib/hanami/db/gem_inflector.rb index 733f0d4..d949980 100644 --- a/lib/hanami/db/gem_inflector.rb +++ b/lib/hanami/db/gem_inflector.rb @@ -6,6 +6,7 @@ module DB class GemInflector < Zeitwerk::GemInflector def camelize(basename, _abspath) return "DB" if basename == "db" + return "SQLite" if basename == "sqlite" super end diff --git a/lib/hanami/db/sqlite.rb b/lib/hanami/db/sqlite.rb new file mode 100644 index 0000000..20d5f0e --- /dev/null +++ b/lib/hanami/db/sqlite.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Hanami + module DB + module SQLite + end + end +end diff --git a/lib/hanami/db/sqlite/unknown_pragma_error.rb b/lib/hanami/db/sqlite/unknown_pragma_error.rb new file mode 100644 index 0000000..a45e4bf --- /dev/null +++ b/lib/hanami/db/sqlite/unknown_pragma_error.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Hanami + module DB + module SQLite + class UnknownPragmaError < StandardError + attr_reader :unknown + + def initialize(unknown) + @unknown = unknown.dup.freeze + super(build_message(@unknown)) + end + + private + + def build_message(unknown) + "Unknown SQLite pragma(s): #{unknown.join(", ")}. " \ + "If you have confirmed this pragma is supported by your SQLite build, " \ + "construct Hanami::DB::SQLite::Pragmas with `validate: false` to bypass " \ + "name validation." + end + end + end + end +end diff --git a/spec/unit/sqlite/unknown_pragma_error_spec.rb b/spec/unit/sqlite/unknown_pragma_error_spec.rb new file mode 100644 index 0000000..6e00239 --- /dev/null +++ b/spec/unit/sqlite/unknown_pragma_error_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.describe Hanami::DB::SQLite::UnknownPragmaError do + subject { described_class.new(unknown_pragmas) } + + let(:unknown_pragmas) { [:frobnicate, :wibble] } + + it "is a StandardError" do + expect(subject).to be_a(StandardError) + end + + it "lists the unknown pragma names in the message" do + expect(subject.message).to include("frobnicate") + expect(subject.message).to include("wibble") + end + + it "mentions the validation opt-out" do + expect(subject.message).to include("validate: false") + end + + it "exposes the unknown pragma names" do + expect(subject.unknown).to eq([:frobnicate, :wibble]) + end + + it "exposes a frozen copy of the unknown pragma names" do + expect(subject.unknown).to be_frozen + end + + context "with a single unknown pragma" do + subject { described_class.new([:frobnicate]) } + + it "formats the name plainly in the message" do + expect(subject.message).to include("frobnicate") + end + + it "does not render the names as an inspected array" do + expect(subject.message).not_to include("[:frobnicate]") + end + end +end From d6d36e38c945ee80f82dc25c85c58a5639c2821a Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 11 May 2026 00:30:59 -0600 Subject: [PATCH 2/9] Add Hanami::DB::SQLite::Pragmas with defaults, validation, and connect_sqls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A small value class that holds a set of SQLite pragmas. Defaults are chosen to match the broadly-accepted production-friendly set (WAL, synchronous=normal, 128MiB mmap, 64MiB journal cap, 2k-page cache, foreign keys on); user overrides merge on top, with `clear_defaults` for callers who want to start empty. Name validation runs against the linked SQLite's own `pragma_list` (via `pragma_pragma_list`), reflecting whatever the runtime supports rather than a hardcoded allowlist that would rot. The lookup is class-level, lazy, mutex-guarded, and skipped entirely when `validate: false` — so loading the file never opens a SQLite handle, and concurrent first-callers from a thread-pool boot can't race. `#connect_sqls` returns an array of `PRAGMA name = value` strings suitable for Sequel's per-connection `:connect_sqls` option. That hook fires for every new physical connection the pool opens, which is what pragma state needs — `:after_connect` would have been the wrong abstraction since it receives the raw adapter connection (SQLite3::Database), not a Sequel::Database, and connection-pool semantics mean only one pool member would have seen any one-shot side-effect. String keys in overrides are normalised to symbols so the canonical form (matching DEFAULTS) is the only thing that reaches the rest of the class. --- lib/hanami/db/sqlite/pragmas.rb | 64 ++++++++++++ spec/unit/sqlite/pragmas_spec.rb | 168 +++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 lib/hanami/db/sqlite/pragmas.rb create mode 100644 spec/unit/sqlite/pragmas_spec.rb diff --git a/lib/hanami/db/sqlite/pragmas.rb b/lib/hanami/db/sqlite/pragmas.rb new file mode 100644 index 0000000..d79d82b --- /dev/null +++ b/lib/hanami/db/sqlite/pragmas.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require "sqlite3" + +module Hanami + module DB + module SQLite + class Pragmas + DEFAULTS = { + foreign_keys: true, + journal_mode: :wal, + synchronous: :normal, + mmap_size: 128 * 1024 * 1024, + journal_size_limit: 64 * 1024 * 1024, + cache_size: 2_000, + }.freeze + + NAMES_MUTEX = Mutex.new + private_constant :NAMES_MUTEX + + # The authoritative set of pragma names SQLite recognises, queried + # via `PRAGMA pragma_list` (here through its table-valued form, + # `pragma_pragma_list`) against a transient in-memory connection + # on first access. Reflects whatever SQLite the runtime has linked, + # so newer pragmas appear automatically without a gem upgrade. + # Memoized at the class level; the in-memory handle is opened at + # most once per class load, and not at all if `self.names` is + # never called. The mutex guards concurrent first access (e.g. + # parallel connection warmup at boot). + def self.names + @names || NAMES_MUTEX.synchronize do + @names ||= begin + db = ::SQLite3::Database.new(":memory:") + db.execute("SELECT name FROM pragma_pragma_list").flatten.map(&:to_sym).to_set.freeze + ensure + db&.close + end + end + end + + def initialize(overrides: {}, clear_defaults: false, validate: true) + base = clear_defaults ? {} : DEFAULTS + @resolved = base.merge(overrides.transform_keys(&:to_sym)).freeze + validate_names! if validate + end + + def to_h + @resolved + end + + def connect_sqls + @resolved.map { |name, value| "PRAGMA #{name} = #{value}" } + end + + private + + def validate_names! + unknown = @resolved.keys.reject { self.class.names.include?(_1) } + raise UnknownPragmaError.new(unknown) unless unknown.empty? + end + end + end + end +end diff --git a/spec/unit/sqlite/pragmas_spec.rb b/spec/unit/sqlite/pragmas_spec.rb new file mode 100644 index 0000000..820a75e --- /dev/null +++ b/spec/unit/sqlite/pragmas_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +RSpec.describe Hanami::DB::SQLite::Pragmas do + subject { described_class.new(**args) } + + let(:args) { {} } + + describe "DEFAULTS" do + it "is frozen" do + expect(described_class::DEFAULTS).to be_frozen + end + + it "is the chosen set of pragmas" do + expect(described_class::DEFAULTS).to eq( + foreign_keys: true, + journal_mode: :wal, + synchronous: :normal, + mmap_size: 128 * 1024 * 1024, + journal_size_limit: 64 * 1024 * 1024, + cache_size: 2_000, + ) + end + end + + describe "#to_h" do + context "with no arguments" do + it "returns the default set" do + expect(subject.to_h).to eq(described_class::DEFAULTS) + end + end + + context "with overrides" do + let(:args) { {overrides: {synchronous: "full", cache_size: 4_000}} } + + it "overrides matching defaults" do + expect(subject.to_h.fetch(:synchronous)).to eq("full") + end + + it "replaces overridden default values" do + expect(subject.to_h.fetch(:cache_size)).to eq(4_000) + end + + it "keeps unaffected defaults" do + expect(subject.to_h.fetch(:journal_mode)).to eq(:wal) + end + end + + context "with clear_defaults: true" do + let(:args) { {clear_defaults: true, overrides: {foreign_keys: 1}} } + + it "drops the entire default set" do + expect(subject.to_h.keys).to eq([:foreign_keys]) + end + end + + context "with clear_defaults: true and no overrides" do + let(:args) { {clear_defaults: true} } + + it "produces an empty resolved set" do + expect(subject.to_h).to be_empty + end + end + + context "with string-keyed overrides" do + let(:args) { {overrides: {"synchronous" => "full"}} } + + it "normalizes the key to a symbol" do + expect(subject.to_h.fetch(:synchronous)).to eq("full") + end + + it "does not leak the string key into the resolved set" do + expect(subject.to_h.keys).to all(be_a(Symbol)) + end + end + end + + describe "validation" do + context "with an unknown override pragma name" do + let(:args) { {overrides: {frobnicate: 1}} } + + it "raises UnknownPragmaError" do + expect { subject }.to raise_error( + Hanami::DB::SQLite::UnknownPragmaError, + /frobnicate/, + ) + end + + it "lists every unknown name when multiple are given" do + multi_args = {overrides: {frobnicate: 1, wibble: 2}} + expect { described_class.new(**multi_args) }.to raise_error( + Hanami::DB::SQLite::UnknownPragmaError, + ) { |e| expect(e.unknown).to contain_exactly(:frobnicate, :wibble) } + end + end + + context "with a known override pragma name" do + let(:args) { {overrides: {temp_store: "memory"}} } + + it "does not raise" do + expect { subject }.not_to raise_error + end + end + + context "with clear_defaults: true and an unknown override" do + let(:args) { {clear_defaults: true, overrides: {frobnicate: 1}} } + + it "still raises UnknownPragmaError" do + expect { subject }.to raise_error( + Hanami::DB::SQLite::UnknownPragmaError, + /frobnicate/, + ) + end + end + + context "with validate: false and an unknown override" do + let(:args) { {overrides: {frobnicate: 1}, validate: false} } + + it "does not raise" do + expect { subject }.not_to raise_error + end + + it "still includes the unknown pragma in the resolved set" do + expect(subject.to_h.fetch(:frobnicate)).to eq(1) + end + end + end + + describe ".names" do + let(:fake_db) { instance_double(SQLite3::Database, execute: [["foreign_keys"]], close: nil) } + + before { described_class.instance_variable_set(:@names, nil) } + after { described_class.instance_variable_set(:@names, nil) } + + it "opens the :memory: connection only once across many validations" do + allow(SQLite3::Database).to receive(:new).and_return(fake_db) + + 3.times { described_class.new(clear_defaults: true, overrides: {foreign_keys: 1}) } + + expect(SQLite3::Database).to have_received(:new).once + end + + it "does not open a connection when no instance ever validates" do + allow(SQLite3::Database).to receive(:new) + + described_class.new(overrides: {frobnicate: 1}, validate: false) + + expect(SQLite3::Database).not_to have_received(:new) + end + end + + describe "#connect_sqls" do + let(:args) { {overrides: {synchronous: :full}} } + + it "returns one `PRAGMA name = value` statement per resolved pragma" do + expect(subject.connect_sqls).to contain_exactly( + *subject.to_h.map { |name, value| "PRAGMA #{name} = #{value}" }, + ) + end + + it "interpolates overridden values" do + expect(subject.connect_sqls).to include("PRAGMA synchronous = full") + end + + it "preserves the resolved insertion order" do + expect(subject.connect_sqls.first).to eq("PRAGMA foreign_keys = true") + end + end +end From 8f2bcb3ad46e3e39a088f77c02316022225b14ff Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 11 May 2026 00:31:08 -0600 Subject: [PATCH 3/9] Add integration spec covering pragmas applied via Sequel's connect_sqls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit specs exercise the SQL-string shape; this exercises the contract end-to-end. Opens a file-backed SQLite via Sequel with `connect_sqls: pragmas.connect_sqls`, then reads back each PRAGMA to confirm the value SQLite actually committed — including overrides taking precedence and unrelated defaults still applying. Also asserts that pool members beyond the first see the same pragma state, which is the load-bearing property of the per-connection hook (and the reason an :after_connect-style one-shot would have been the wrong design). Adds a `sqlite_file_database_url` helper alongside the existing memory URL helper, so tests can spin up disposable file-backed databases on both MRI and JRuby. --- spec/integration/sqlite/pragmas_spec.rb | 48 +++++++++++++++++++++++++ spec/support/helpers.rb | 8 +++++ 2 files changed, 56 insertions(+) create mode 100644 spec/integration/sqlite/pragmas_spec.rb diff --git a/spec/integration/sqlite/pragmas_spec.rb b/spec/integration/sqlite/pragmas_spec.rb new file mode 100644 index 0000000..7badd9d --- /dev/null +++ b/spec/integration/sqlite/pragmas_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "sequel" +require "tempfile" + +RSpec.describe "SQLite Pragmas integration" do + subject(:pragmas) { Hanami::DB::SQLite::Pragmas.new(overrides: overrides) } + + let(:overrides) { {synchronous: :full} } + let(:tempfile) { Tempfile.new(["hanami-db-pragmas", ".sqlite"]) } + let(:db) do + Sequel.connect(sqlite_file_database_url(tempfile.path), connect_sqls: pragmas.connect_sqls) + end + + after do + db.disconnect + tempfile.close + tempfile.unlink + end + + def pragma(name) + db.fetch("PRAGMA #{name}").first&.fetch(name) + end + + it "sets foreign_keys" do + expect(pragma(:foreign_keys).to_i).to eq(1) + end + + it "sets journal_mode to wal" do + expect(pragma(:journal_mode)).to eq("wal") + end + + it "applies user overrides over defaults" do + expect(pragma(:synchronous).to_i).to eq(2) # full = 2 + end + + it "leaves unrelated defaults applied" do + expect(pragma(:cache_size).to_i).to eq(2_000) + end + + it "applies the pragmas to every new pool connection, not just the first" do + # Force a second physical connection to be opened from the pool. + db.pool.hold { |_conn| } + db.pool.hold { |_conn| } + + expect(pragma(:journal_mode)).to eq("wal") + end +end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index 779f53c..0499c95 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -9,6 +9,14 @@ def sqlite_memory_database_url "sqlite:file::memory:?cache=private" end end + + def sqlite_file_database_url(path) + if RUBY_PLATFORM == "java" + "jdbc:sqlite:#{path}" + else + "sqlite://#{path}" + end + end end end From ddd794c7a5fde19caf0c0c863327374045e334df Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 11 May 2026 00:31:13 -0600 Subject: [PATCH 4/9] Note Hanami::DB::SQLite::Pragmas in the changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b68bd6..d8a1f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Break Versioning](https://www.taoensso.com/break-ve ### Added +- `Hanami::DB::SQLite::Pragmas` — a value class holding a set of SQLite + pragmas with sensible defaults, user overrides, and validation against + `PRAGMA pragma_list`. Exposes `#connect_sqls`, an array of `PRAGMA` + statements suitable for Sequel's per-connection `connect_sqls` option. + `UnknownPragmaError` is raised when an override names a pragma the + linked SQLite doesn't recognise. + ### Changed ### Deprecated From 3b0b374508310c03c14b444fd5b29d2acdec1d29 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Tue, 12 May 2026 00:02:23 -0600 Subject: [PATCH 5/9] Make Pragmas name validation mandatory Drops the `validate:` kwarg. Validation now runs unconditionally on every `Pragmas.new`, so a typo'd pragma name always raises rather than depending on how the caller constructed the instance. The bypass guidance is removed from `UnknownPragmaError`'s message and the corresponding `validate: false` test contexts go with it. --- lib/hanami/db/sqlite/pragmas.rb | 9 ++++----- lib/hanami/db/sqlite/unknown_pragma_error.rb | 5 +---- spec/unit/sqlite/pragmas_spec.rb | 19 ------------------- spec/unit/sqlite/unknown_pragma_error_spec.rb | 4 ---- 4 files changed, 5 insertions(+), 32 deletions(-) diff --git a/lib/hanami/db/sqlite/pragmas.rb b/lib/hanami/db/sqlite/pragmas.rb index d79d82b..6bbd609 100644 --- a/lib/hanami/db/sqlite/pragmas.rb +++ b/lib/hanami/db/sqlite/pragmas.rb @@ -24,9 +24,8 @@ class Pragmas # on first access. Reflects whatever SQLite the runtime has linked, # so newer pragmas appear automatically without a gem upgrade. # Memoized at the class level; the in-memory handle is opened at - # most once per class load, and not at all if `self.names` is - # never called. The mutex guards concurrent first access (e.g. - # parallel connection warmup at boot). + # most once per class load. The mutex guards concurrent first + # access (e.g. parallel connection warmup at boot). def self.names @names || NAMES_MUTEX.synchronize do @names ||= begin @@ -38,10 +37,10 @@ def self.names end end - def initialize(overrides: {}, clear_defaults: false, validate: true) + def initialize(overrides: {}, clear_defaults: false) base = clear_defaults ? {} : DEFAULTS @resolved = base.merge(overrides.transform_keys(&:to_sym)).freeze - validate_names! if validate + validate_names! end def to_h diff --git a/lib/hanami/db/sqlite/unknown_pragma_error.rb b/lib/hanami/db/sqlite/unknown_pragma_error.rb index a45e4bf..ca4f50d 100644 --- a/lib/hanami/db/sqlite/unknown_pragma_error.rb +++ b/lib/hanami/db/sqlite/unknown_pragma_error.rb @@ -14,10 +14,7 @@ def initialize(unknown) private def build_message(unknown) - "Unknown SQLite pragma(s): #{unknown.join(", ")}. " \ - "If you have confirmed this pragma is supported by your SQLite build, " \ - "construct Hanami::DB::SQLite::Pragmas with `validate: false` to bypass " \ - "name validation." + "Unknown SQLite pragma(s): #{unknown.join(", ")}." end end end diff --git a/spec/unit/sqlite/pragmas_spec.rb b/spec/unit/sqlite/pragmas_spec.rb index 820a75e..435981b 100644 --- a/spec/unit/sqlite/pragmas_spec.rb +++ b/spec/unit/sqlite/pragmas_spec.rb @@ -112,17 +112,6 @@ end end - context "with validate: false and an unknown override" do - let(:args) { {overrides: {frobnicate: 1}, validate: false} } - - it "does not raise" do - expect { subject }.not_to raise_error - end - - it "still includes the unknown pragma in the resolved set" do - expect(subject.to_h.fetch(:frobnicate)).to eq(1) - end - end end describe ".names" do @@ -138,14 +127,6 @@ expect(SQLite3::Database).to have_received(:new).once end - - it "does not open a connection when no instance ever validates" do - allow(SQLite3::Database).to receive(:new) - - described_class.new(overrides: {frobnicate: 1}, validate: false) - - expect(SQLite3::Database).not_to have_received(:new) - end end describe "#connect_sqls" do diff --git a/spec/unit/sqlite/unknown_pragma_error_spec.rb b/spec/unit/sqlite/unknown_pragma_error_spec.rb index 6e00239..6bf0a01 100644 --- a/spec/unit/sqlite/unknown_pragma_error_spec.rb +++ b/spec/unit/sqlite/unknown_pragma_error_spec.rb @@ -14,10 +14,6 @@ expect(subject.message).to include("wibble") end - it "mentions the validation opt-out" do - expect(subject.message).to include("validate: false") - end - it "exposes the unknown pragma names" do expect(subject.unknown).to eq([:frobnicate, :wibble]) end From 034ee07cda4c496f38d96c6e7cb89a7bec49e0c1 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Tue, 12 May 2026 00:03:57 -0600 Subject: [PATCH 6/9] Rubocop --- lib/hanami/db/sqlite/pragmas.rb | 2 +- spec/unit/sqlite/pragmas_spec.rb | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/hanami/db/sqlite/pragmas.rb b/lib/hanami/db/sqlite/pragmas.rb index 6bbd609..3a9a141 100644 --- a/lib/hanami/db/sqlite/pragmas.rb +++ b/lib/hanami/db/sqlite/pragmas.rb @@ -12,7 +12,7 @@ class Pragmas synchronous: :normal, mmap_size: 128 * 1024 * 1024, journal_size_limit: 64 * 1024 * 1024, - cache_size: 2_000, + cache_size: 2_000 }.freeze NAMES_MUTEX = Mutex.new diff --git a/spec/unit/sqlite/pragmas_spec.rb b/spec/unit/sqlite/pragmas_spec.rb index 435981b..cc1f1c2 100644 --- a/spec/unit/sqlite/pragmas_spec.rb +++ b/spec/unit/sqlite/pragmas_spec.rb @@ -17,7 +17,7 @@ synchronous: :normal, mmap_size: 128 * 1024 * 1024, journal_size_limit: 64 * 1024 * 1024, - cache_size: 2_000, + cache_size: 2_000 ) end end @@ -81,14 +81,14 @@ it "raises UnknownPragmaError" do expect { subject }.to raise_error( Hanami::DB::SQLite::UnknownPragmaError, - /frobnicate/, + /frobnicate/ ) end it "lists every unknown name when multiple are given" do multi_args = {overrides: {frobnicate: 1, wibble: 2}} expect { described_class.new(**multi_args) }.to raise_error( - Hanami::DB::SQLite::UnknownPragmaError, + Hanami::DB::SQLite::UnknownPragmaError ) { |e| expect(e.unknown).to contain_exactly(:frobnicate, :wibble) } end end @@ -107,11 +107,10 @@ it "still raises UnknownPragmaError" do expect { subject }.to raise_error( Hanami::DB::SQLite::UnknownPragmaError, - /frobnicate/, + /frobnicate/ ) end end - end describe ".names" do @@ -134,7 +133,7 @@ it "returns one `PRAGMA name = value` statement per resolved pragma" do expect(subject.connect_sqls).to contain_exactly( - *subject.to_h.map { |name, value| "PRAGMA #{name} = #{value}" }, + *subject.to_h.map { |name, value| "PRAGMA #{name} = #{value}" } ) end From e08ad6674c17fdf9e133b7139cfdada54e172615 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Tue, 12 May 2026 00:23:18 -0600 Subject: [PATCH 7/9] Route Pragmas.names lookup through Sequel for JRuby compatibility JRuby installs `jdbc-sqlite3`, not the `sqlite3` gem, so the eager `require "sqlite3"` and the direct `SQLite3::Database` reference broke spec loading entirely on JRuby (LoadError before any example ran). Sequel is already a transitive runtime dependency via `rom-sql` and abstracts the adapter difference behind a URL scheme. Opening a `:memory:` connection through Sequel uses sqlite3-ruby on MRI and jdbc-sqlite3 on JRuby, both yielding the same pragma_list contents. --- lib/hanami/db/sqlite/pragmas.rb | 11 ++++++----- spec/unit/sqlite/pragmas_spec.rb | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/hanami/db/sqlite/pragmas.rb b/lib/hanami/db/sqlite/pragmas.rb index 3a9a141..9eff75f 100644 --- a/lib/hanami/db/sqlite/pragmas.rb +++ b/lib/hanami/db/sqlite/pragmas.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "sqlite3" - module Hanami module DB module SQLite @@ -18,6 +16,9 @@ class Pragmas NAMES_MUTEX = Mutex.new private_constant :NAMES_MUTEX + MEMORY_URL = RUBY_PLATFORM == "java" ? "jdbc:sqlite::memory:" : "sqlite::memory:" + private_constant :MEMORY_URL + # The authoritative set of pragma names SQLite recognises, queried # via `PRAGMA pragma_list` (here through its table-valued form, # `pragma_pragma_list`) against a transient in-memory connection @@ -29,10 +30,10 @@ class Pragmas def self.names @names || NAMES_MUTEX.synchronize do @names ||= begin - db = ::SQLite3::Database.new(":memory:") - db.execute("SELECT name FROM pragma_pragma_list").flatten.map(&:to_sym).to_set.freeze + db = Sequel.connect(MEMORY_URL) + db.fetch("SELECT name FROM pragma_pragma_list").map { |row| row[:name].to_sym }.to_set.freeze ensure - db&.close + db&.disconnect end end end diff --git a/spec/unit/sqlite/pragmas_spec.rb b/spec/unit/sqlite/pragmas_spec.rb index cc1f1c2..fe97551 100644 --- a/spec/unit/sqlite/pragmas_spec.rb +++ b/spec/unit/sqlite/pragmas_spec.rb @@ -114,17 +114,17 @@ end describe ".names" do - let(:fake_db) { instance_double(SQLite3::Database, execute: [["foreign_keys"]], close: nil) } + let(:fake_db) { instance_double(Sequel::Database, fetch: [{name: "foreign_keys"}], disconnect: nil) } before { described_class.instance_variable_set(:@names, nil) } after { described_class.instance_variable_set(:@names, nil) } it "opens the :memory: connection only once across many validations" do - allow(SQLite3::Database).to receive(:new).and_return(fake_db) + allow(Sequel).to receive(:connect).and_return(fake_db) 3.times { described_class.new(clear_defaults: true, overrides: {foreign_keys: 1}) } - expect(SQLite3::Database).to have_received(:new).once + expect(Sequel).to have_received(:connect).once end end From ae0289c8a2ca4cfc7a60506983e3ae57eb712bd0 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Tue, 12 May 2026 00:25:27 -0600 Subject: [PATCH 8/9] Switch JRuby detection from RUBY_PLATFORM to RUBY_ENGINE RUBY_PLATFORM describes the underlying machine ("x86_64-linux", "x86_64-darwin", "java"); JRuby reporting "java" there is an implementation quirk, not the field's purpose. RUBY_ENGINE names the implementation directly ("ruby", "jruby", "truffleruby"), which is what every check in this repo actually means. --- lib/hanami/db/sqlite/pragmas.rb | 2 +- spec/support/helpers.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/hanami/db/sqlite/pragmas.rb b/lib/hanami/db/sqlite/pragmas.rb index 9eff75f..ddfa65c 100644 --- a/lib/hanami/db/sqlite/pragmas.rb +++ b/lib/hanami/db/sqlite/pragmas.rb @@ -16,7 +16,7 @@ class Pragmas NAMES_MUTEX = Mutex.new private_constant :NAMES_MUTEX - MEMORY_URL = RUBY_PLATFORM == "java" ? "jdbc:sqlite::memory:" : "sqlite::memory:" + MEMORY_URL = RUBY_ENGINE == "jruby" ? "jdbc:sqlite::memory:" : "sqlite::memory:" private_constant :MEMORY_URL # The authoritative set of pragma names SQLite recognises, queried diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index 0499c95..4b8017f 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -3,7 +3,7 @@ module Test module Helpers def sqlite_memory_database_url - if RUBY_PLATFORM == "java" + if RUBY_ENGINE == "jruby" "jdbc:sqlite:file::memory:?cache=private" else "sqlite:file::memory:?cache=private" @@ -11,7 +11,7 @@ def sqlite_memory_database_url end def sqlite_file_database_url(path) - if RUBY_PLATFORM == "java" + if RUBY_ENGINE == "jruby" "jdbc:sqlite:#{path}" else "sqlite://#{path}" From e3d083c8066ce83334d0e97c5baa5d71acd494d5 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Tue, 12 May 2026 00:26:00 -0600 Subject: [PATCH 9/9] Lift MEMORY_URL to the Hanami::DB::SQLite namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The :memory: SQLite URL isn't a Pragmas concern — it's a SQLite-level fact about which adapter scheme this Ruby implementation supports. Hoisting it makes it reachable for any other internal SQLite code that needs a transient in-memory connection (e.g. future schema inspection or feature detection), and shrinks the Pragmas surface. Lexical constant lookup still resolves `MEMORY_URL` cleanly from within Pragmas, so no qualifier needed at the call site. --- lib/hanami/db/sqlite.rb | 1 + lib/hanami/db/sqlite/pragmas.rb | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/hanami/db/sqlite.rb b/lib/hanami/db/sqlite.rb index 20d5f0e..69d919c 100644 --- a/lib/hanami/db/sqlite.rb +++ b/lib/hanami/db/sqlite.rb @@ -3,6 +3,7 @@ module Hanami module DB module SQLite + MEMORY_URL = RUBY_ENGINE == "jruby" ? "jdbc:sqlite::memory:" : "sqlite::memory:" end end end diff --git a/lib/hanami/db/sqlite/pragmas.rb b/lib/hanami/db/sqlite/pragmas.rb index ddfa65c..251543b 100644 --- a/lib/hanami/db/sqlite/pragmas.rb +++ b/lib/hanami/db/sqlite/pragmas.rb @@ -16,9 +16,6 @@ class Pragmas NAMES_MUTEX = Mutex.new private_constant :NAMES_MUTEX - MEMORY_URL = RUBY_ENGINE == "jruby" ? "jdbc:sqlite::memory:" : "sqlite::memory:" - private_constant :MEMORY_URL - # The authoritative set of pragma names SQLite recognises, queried # via `PRAGMA pragma_list` (here through its table-valued form, # `pragma_pragma_list`) against a transient in-memory connection