Module: Tina4::DatabaseAdapter
- Included in:
- Tina4::Drivers::FirebirdDriver, Tina4::Drivers::MongodbDriver, Tina4::Drivers::MssqlDriver, Tina4::Drivers::MysqlDriver, Tina4::Drivers::OdbcDriver, Tina4::Drivers::PostgresDriver, Tina4::Drivers::SqliteDriver
- Defined in:
- lib/tina4/database_adapter.rb
Overview
The contract every database driver must satisfy.
Feature 3 of the feature audit. Ruby had NO adapter interface: Database
called four things on a driver and guarded the rest behind six respond_to?
checks. The consequences, in order of severity:
- A driver missing a method was discovered at runtime, on whichever engine nobody exercised, and the guards meant the failure was often a SILENT SKIP rather than an exception - which is worse than a crash.
- Nothing told a contributor writing an eighth driver what to implement. The answer was "read database.rb and infer", and it is 828 lines.
- The audit could not compare Ruby's contract to the other three, because Ruby did not have one. That was the finding.
Measured against the shared contract (spec/fixtures/adapter_contract.json, byte-identical in all four), the seven drivers scored 9, 10 and 11 out of 20
- three different levels of completeness, because each implemented whatever its facade path happened to need.
Every method here raises. A driver that does not override one fails LOUDLY, at the point of the call, naming itself and the method - instead of being quietly skipped.
Migration in progress
The owner's decision (2026-07-30) is that CRUD lives on the ADAPTER, matching
PHP, Python and Node. Today Ruby's facade builds the SQL for fetch / insert /
update / delete and calls the driver's execute, consulting drv.insert
only when a driver chooses to own it (PostgreSQL does, via RETURNING *).
Those methods are declared here so the gap is visible and countable; they are
migrated driver by driver, each with its own test, rather than in one sweep.
Until a driver overrides them, Database keeps using its own path - see
Database#driver_implements?, which asks whether the driver actually
OVERRODE a method rather than whether it merely responds to it. That
distinction is the whole point: including this module makes every driver
respond to everything, so respond_to? stopped being able to tell the
difference.
Constant Summary collapse
- CONTRACT =
Methods a driver MUST override. Kept as data so the conformance spec can read it instead of maintaining a second copy of the list. The REDESIGNED contract: only what genuinely differs per engine.
CRUD (insert/update/delete), executeMany, fetchOne and DDL (create_table/add_column) are NOT here. They are composable above the adapter from execute + fetch + get_database_type, and Ruby was already doing exactly that in the facade - which is why Ruby's driver layer is 1335 LOC against PHP's 5823 for the same job. The first contract this row produced would have made Ruby write those seven more times; this one keeps the shape Ruby already had and asks the other three to adopt it.
%i[ open close get_database_type execute fetch start_transaction commit rollback autocommit get_tables get_columns table_exists last_insert_id error ].freeze
- CONNECT_TIMEOUT_VAR =
Bounding the connect
A connect that can block forever hangs the whole application with NO log, no error and no signal. MEASURED here on Ruby 3.2.3 / Ubuntu 24.04.4 against a real TCPServer that accepts the TCP connection and then never replies: pg, mysql2, tiny_tds AND fb all sat past 20 seconds and needed SIGKILL -
timeout's SIGTERM could not even be delivered, because the blocking work happens inside a C client that never yields to the interpreter. (A CLOSED port is a different thing entirely: it refuses in 0.00s and tests nothing.)ONE variable governs every driver whose connect crosses a network:
TINA4_DATABASE_CONNECT_TIMEOUT seconds, default 10; <= 0 disables the bound (unbounded, the old behaviour); a non-number warns and falls back to 10Each driver applies it through its OWN native option - libpq connect_timeout, mysql2 connect_timeout, FreeTDS login_timeout, mongo connect_timeout - because only the C client can interrupt its own blocking socket work. Ruby's Timeout.timeout and Thread#join CANNOT: see Tina4::Drivers::FirebirdDriver.bound_reachability! for the measurement.
There is deliberately NO outer Ruby timeout racing the native one. The native option is the ONLY timer; bounding_connect below merely TRANSLATES whatever the client raises into the one contract message, so the operator is never left holding a driver-worded error that names no variable. Where a driver cannot produce that message at all, its own file says so at the point of exclusion:
postgres bounded + contract message libpq connect_timeout mysql bounded + contract message mysql2 connect_timeout mssql bounded + contract message FreeTDS login_timeout firebird bounded to REACHABILITY only stdlib socket; the attach itself cannot be bounded from Ruby mongodb bounded, NO message possible Client.new never fails sqlite n/a local file, no network peer odbc NOT bounded gem untestable here; see its file "TINA4_DATABASE_CONNECT_TIMEOUT"- DEFAULT_CONNECT_TIMEOUT_SECONDS =
10- CONNECT_TIMEOUT_SLACK_SECONDS =
Clock slack when deciding whether a failed connect was OUR bound expiring. A native bound of 10s is measured back as 9.998s often enough to matter, and without the slack the contract error would degrade into the raw driver error at random.
0.25
Class Method Summary collapse
-
.bounding_connect(host, port) ⇒ Object
Run a driver's natively-bounded connect and translate an expiry into the contract error above.
-
.connect_timed_out!(host, port, elapsed_seconds, cause = nil) ⇒ Object
The one error a timed-out connect raises: it names the host, the port, the seconds actually spent, and the variable that tunes it.
-
.connect_timeout_seconds ⇒ Object
Seconds to bound a connect by, or nil when the operator disabled the bound.
-
.connect_timeout_whole_seconds ⇒ Object
Whole seconds for the native options that accept only an integer (libpq, libmysqlclient, FreeTDS).
-
.implemented_by?(object, name) ⇒ Boolean
Did this driver actually OVERRIDE the contract method, or is it inheriting the raising stub?
respond_to?cannot answer that once the module is included, and answering it wrongly turns a working silent-skip path into a NotImplementedError at runtime.
Class Method Details
.bounding_connect(host, port) ⇒ Object
Run a driver's natively-bounded connect and translate an expiry into the contract error above. The NATIVE option does the bounding; this only names it. Whether the bound expired is decided by ELAPSED TIME, not by matching driver error text - the four clients word it four different ways ("timeout expired", "waiting for initial communication packet", "TDS server connection timed out", "Connection timed out"), and a marker table is one more thing to drift and MISS. A missed timeout is the whole defect.
150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/tina4/database_adapter.rb', line 150 def self.bounding_connect(host, port) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) yield rescue StandardError => error bound = connect_timeout_seconds raise if bound.nil? elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at raise if elapsed < bound - CONNECT_TIMEOUT_SLACK_SECONDS connect_timed_out!(host, port, elapsed, error) end |
.connect_timed_out!(host, port, elapsed_seconds, cause = nil) ⇒ Object
The one error a timed-out connect raises: it names the host, the port, the seconds actually spent, and the variable that tunes it.
134 135 136 137 138 139 140 141 |
# File 'lib/tina4/database_adapter.rb', line 134 def self.connect_timed_out!(host, port, elapsed_seconds, cause = nil) detail = cause ? " Driver reported: #{cause..to_s.gsub(/\s+/, " ").strip}" : "" raise Tina4::DatabaseConnectionError, "Database connect to #{host}:#{port} timed out after " \ "#{format("%.1f", elapsed_seconds)}s (#{CONNECT_TIMEOUT_VAR}=" \ "#{connect_timeout_seconds} seconds; set it to 0 to wait " \ "indefinitely).#{detail}" end |
.connect_timeout_seconds ⇒ Object
Seconds to bound a connect by, or nil when the operator disabled the bound.
118 119 120 121 |
# File 'lib/tina4/database_adapter.rb', line 118 def self.connect_timeout_seconds seconds = Tina4::Env.float(CONNECT_TIMEOUT_VAR, default: DEFAULT_CONNECT_TIMEOUT_SECONDS) seconds.positive? ? seconds : nil end |
.connect_timeout_whole_seconds ⇒ Object
Whole seconds for the native options that accept only an integer (libpq, libmysqlclient, FreeTDS). Rounds UP and never below 1: libpq reads connect_timeout=0 as "wait forever", so rounding 0.4 DOWN to 0 would silently disable the very bound being set.
127 128 129 130 |
# File 'lib/tina4/database_adapter.rb', line 127 def self.connect_timeout_whole_seconds seconds = connect_timeout_seconds seconds && [seconds.ceil, 1].max end |
.implemented_by?(object, name) ⇒ Boolean
Did this driver actually OVERRIDE the contract method, or is it inheriting
the raising stub? respond_to? cannot answer that once the module is
included, and answering it wrongly turns a working silent-skip path into a
NotImplementedError at runtime.
167 168 169 170 171 172 173 174 175 176 |
# File 'lib/tina4/database_adapter.rb', line 167 def self.implemented_by?(object, name) return false unless object.respond_to?(name) owner = begin object.class.instance_method(name).owner rescue NameError nil end !owner.nil? && owner != self end |