Class: Tina4::Drivers::FirebirdDriver
- Inherits:
-
Object
- Object
- Tina4::Drivers::FirebirdDriver
- Defined in:
- lib/tina4/drivers/firebird_driver.rb
Constant Summary collapse
- DEAD_CONN_MARKERS =
Substring markers (lowercased) that identify a dead-socket Firebird error worth reconnecting for. Idle Firebird connections die silently behind NAT timeouts, server-side ConnectionIdleTimeout, or Docker network rotation; without this the next prepare crashes the request.
[ "error writing data to the connection", "error reading data from the connection", "connection shutdown", "connection lost", "network error", "connection is not active", "broken pipe" ].freeze
- WIN_DRIVE_RE =
Detects a Windows drive-letter prefix like "C:/" or "C:". The leading-slash variant ("/C:/...") shows up after URI.parse strips one slash off "firebird://host:port/C:/...".
%r{\A/?[A-Za-z]:[/\\]}.freeze
Instance Attribute Summary collapse
-
#connection ⇒ Object
readonly
Returns the value of attribute connection.
Class Method Summary collapse
-
.dead_connection?(error_or_message) ⇒ Boolean
Public so specs (and curious operators) can verify the matcher behaviour without poking private methods.
-
.normalize_db_identifier(raw_path) ⇒ Object
Turn the URL path component into a Firebird database identifier.
-
.resolve_charset(connection_string, kwarg_charset = nil) ⇒ Object
Resolve the Firebird connection charset (#160, mirrors php #160 / the Python master's _resolve_firebird_charset).
Instance Method Summary collapse
- #apply_limit(sql, limit, offset = 0) ⇒ Object
- #begin_transaction ⇒ Object
- #close ⇒ Object
- #columns(table_name) ⇒ Object
- #commit ⇒ Object
- #connect(connection_string, username: nil, password: nil, charset: nil) ⇒ Object
- #execute(sql, params = []) ⇒ Object
- #execute_query(sql, params = []) ⇒ Object
- #last_insert_id ⇒ Object
- #placeholder ⇒ Object
- #placeholders(count) ⇒ Object
- #rollback ⇒ Object
- #tables ⇒ Object
Instance Attribute Details
#connection ⇒ Object (readonly)
Returns the value of attribute connection.
6 7 8 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 6 def connection @connection end |
Class Method Details
.dead_connection?(error_or_message) ⇒ Boolean
Public so specs (and curious operators) can verify the matcher behaviour without poking private methods.
193 194 195 196 197 198 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 193 def self.dead_connection?() msg = .respond_to?(:message) ? . : .to_s return false if msg.nil? || msg.empty? lower = msg.downcase DEAD_CONN_MARKERS.any? { |m| lower.include?(m) } end |
.normalize_db_identifier(raw_path) ⇒ Object
Turn the URL path component into a Firebird database identifier.
Firebird is the awkward one — it needs either an absolute file path on the server, a Windows drive-letter path, or an alias name. The classic URI form uses a double-slash to keep the leading "/" of an absolute path through URI.parse:
firebird://host:port//firebird/data/app.fdb → /firebird/data/app.fdb
But that double slash is unintuitive to anyone used to the way postgres / mysql / mssql encode the database name. We accept five equivalent forms and normalise all of them:
- "//abs/path/db.fdb" → "/abs/path/db.fdb" (classic double-slash)
- "/abs/path/db.fdb" → "/abs/path/db.fdb" (single-slash, what most people type)
- "/C:/Data/db.fdb" → "C:/Data/db.fdb" (Windows, leading URL slash dropped)
- "/C%3A/Data/db.fdb" → "C:/Data/db.fdb" (Windows with URL-encoded colon)
- "/employee" → "employee" (alias — single token)
Aliases are detected as the leftover case: a single token with no slashes. Anything path-like is kept as a path.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 48 def self.normalize_db_identifier(raw_path) require "uri" return "" if raw_path.nil? || raw_path.empty? decoded = URI.decode_www_form_component(raw_path) # Classic double-slash form: //abs/path → /abs/path decoded = decoded[1..] if decoded.start_with?("//") # Windows drive-letter — drop the URL-introduced leading slash. # /C:/Data/db.fdb → C:/Data/db.fdb if WIN_DRIVE_RE.match?(decoded) decoded = decoded[1..] if decoded.start_with?("/") return decoded end # Look at the content after stripping the leading slash. If it's a # single token with no separators, it's a Firebird alias — return # WITHOUT the leading slash (the alias name itself is the identifier). body = decoded.start_with?("/") ? decoded[1..] : decoded if !body.empty? && !body.include?("/") && !body.include?("\\") return body end # Otherwise it's a file path. If it already has a leading slash, # keep it. If it's a relative-looking path (slash-separated but no # leading "/") promote it to absolute — Firebird needs absolute paths # and we don't know the server's CWD anyway. decoded.start_with?("/") ? decoded : "/#{decoded}" end |
.resolve_charset(connection_string, kwarg_charset = nil) ⇒ Object
Resolve the Firebird connection charset (#160, mirrors php #160 / the Python master's _resolve_firebird_charset).
The driver used to pass NO charset to the fb gem, leaving it to the
gem's own (non-UTF8) default — which double-encodes UTF-8 bytes stored
under a legacy NONE database and diverges from the other frameworks.
The charset is now resolved from, in precedence order:
1. the connection URL query — firebird://host:port/path?charset=NONE
2. an explicit charset: kwarg passed to #connect
3. the TINA4_DATABASE_CHARSET environment variable
4. the UTF8 default (canonical across all four frameworks)
Pure config resolution over its inputs (URL string, kwarg, env) — it
opens NO connection, so it is unit-testable without a live server. A
blank value at any level is treated as absent (matching the Python
master's falsy-string semantics) so ?charset= / an empty env var falls
through rather than connecting with an empty charset.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 97 def self.resolve_charset(connection_string, kwarg_charset = nil) require "uri" url_charset = nil query = begin URI.parse(connection_string.to_s).query rescue URI::InvalidURIError nil end if query && !query.empty? pair = URI.decode_www_form(query).find { |k, _| k == "charset" } url_charset = pair[1] if pair && !pair[1].to_s.empty? end kwarg = kwarg_charset.to_s.empty? ? nil : kwarg_charset env = ENV["TINA4_DATABASE_CHARSET"].to_s.empty? ? nil : ENV["TINA4_DATABASE_CHARSET"] url_charset || kwarg || env || "UTF8" end |
Instance Method Details
#apply_limit(sql, limit, offset = 0) ⇒ Object
212 213 214 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 212 def apply_limit(sql, limit, offset = 0) "SELECT FIRST #{limit} SKIP #{offset} * FROM (#{sql})" end |
#begin_transaction ⇒ Object
216 217 218 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 216 def begin_transaction @transaction = @connection.transaction end |
#close ⇒ Object
166 167 168 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 166 def close @connection&.close end |
#columns(table_name) ⇒ Object
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 234 def columns(table_name) sql = "SELECT RF.RDB\$FIELD_NAME, F.RDB\$FIELD_TYPE, RF.RDB\$NULL_FLAG, RF.RDB\$DEFAULT_SOURCE " \ "FROM RDB\$RELATION_FIELDS RF " \ "JOIN RDB\$FIELDS F ON RF.RDB\$FIELD_SOURCE = F.RDB\$FIELD_NAME " \ "WHERE RF.RDB\$RELATION_NAME = ?" rows = execute_query(sql, [table_name.upcase]) rows.map do |r| { name: (r["RDB\$FIELD_NAME"] || r["rdb\$field_name"] || "").strip, type: r["RDB\$FIELD_TYPE"] || r["rdb\$field_type"], nullable: (r["RDB\$NULL_FLAG"] || r["rdb\$null_flag"]).nil?, default: r["RDB\$DEFAULT_SOURCE"] || r["rdb\$default_source"], primary_key: false } end end |
#commit ⇒ Object
220 221 222 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 220 def commit @transaction&.commit end |
#connect(connection_string, username: nil, password: nil, charset: nil) ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 114 def connect(connection_string, username: nil, password: nil, charset: nil) require "fb" require "uri" uri = URI.parse(connection_string) host = uri.host port = uri.port || 3050 db_user = username || uri.user db_pass = password || uri.password # Firebird database identifier resolution — two layers: # # 1. TINA4_DATABASE_FIREBIRD_PATH env override wins if set. # Useful for Windows users with raw backslash paths (no URL # encoding required) and for ops setups that keep server URL # and DB location in separate config layers. # 2. Otherwise normalise the URL path component — accepts every # sensible variant (single/double slash, drive letter, alias). env_override = ENV["TINA4_DATABASE_FIREBIRD_PATH"].to_s db_path = if !env_override.empty? env_override else self.class.normalize_db_identifier(uri.path.to_s) end database = if host "#{host}/#{port}:#{db_path}" else # No host → fall back to the raw identifier (or, for # totally non-URL inputs, strip the scheme prefix). return_path = db_path return_path = connection_string.sub(/^firebird:\/\//, "") if return_path.empty? return_path end # Cache for transparent reconnect — never logged, lives only in # driver memory alongside the connection it owns. @connect_opts = { database: database } @connect_opts[:username] = db_user if db_user @connect_opts[:password] = db_pass if db_pass # #160: honour ?charset= in the URL, an explicit charset: kwarg, and # TINA4_DATABASE_CHARSET so a legacy NONE database isn't force-connected # with the gem's default charset (double-encoding). Defaults to UTF8. @connect_opts[:charset] = self.class.resolve_charset(connection_string, charset) open_connection rescue LoadError raise LoadError, "The 'fb' gem is required for Firebird connections. Install one of:\n" \ " bundle add fb # if your project uses Bundler\n" \ " gem install fb # bare driver" end |
#execute(sql, params = []) ⇒ Object
181 182 183 184 185 186 187 188 189 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 181 def execute(sql, params = []) with_reconnect do if params.empty? @connection.execute(sql) else @connection.execute(sql, *params) end end end |
#execute_query(sql, params = []) ⇒ Object
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 170 def execute_query(sql, params = []) rows = with_reconnect do if params.empty? @connection.query(:hash, sql) else @connection.query(:hash, sql, *params) end end rows.map { |row| decode_blobs(stringify_keys(row)) } end |
#last_insert_id ⇒ Object
200 201 202 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 200 def last_insert_id nil end |
#placeholder ⇒ Object
204 205 206 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 204 def placeholder "?" end |
#placeholders(count) ⇒ Object
208 209 210 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 208 def placeholders(count) (["?"] * count).join(", ") end |
#rollback ⇒ Object
224 225 226 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 224 def rollback @transaction&.rollback end |
#tables ⇒ Object
228 229 230 231 232 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 228 def tables sql = "SELECT RDB\$RELATION_NAME FROM RDB\$RELATIONS WHERE RDB\$SYSTEM_FLAG = 0 AND RDB\$VIEW_BLR IS NULL" rows = execute_query(sql) rows.map { |r| (r["RDB\$RELATION_NAME"] || r["rdb\$relation_name"] || "").strip } end |