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.
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) ⇒ 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.
151 152 153 154 155 156 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 151 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 |
Instance Method Details
#apply_limit(sql, limit, offset = 0) ⇒ Object
170 171 172 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 170 def apply_limit(sql, limit, offset = 0) "SELECT FIRST #{limit} SKIP #{offset} * FROM (#{sql})" end |
#begin_transaction ⇒ Object
174 175 176 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 174 def begin_transaction @transaction = @connection.transaction end |
#close ⇒ Object
124 125 126 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 124 def close @connection&.close end |
#columns(table_name) ⇒ Object
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 192 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
178 179 180 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 178 def commit @transaction&.commit end |
#connect(connection_string, username: nil, password: nil) ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 79 def connect(connection_string, username: nil, password: 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 open_connection rescue LoadError raise "Firebird driver requires the 'fb' gem. Install it with: gem install fb" end |
#execute(sql, params = []) ⇒ Object
139 140 141 142 143 144 145 146 147 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 139 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
128 129 130 131 132 133 134 135 136 137 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 128 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
158 159 160 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 158 def last_insert_id nil end |
#placeholder ⇒ Object
162 163 164 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 162 def placeholder "?" end |
#placeholders(count) ⇒ Object
166 167 168 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 166 def placeholders(count) (["?"] * count).join(", ") end |
#rollback ⇒ Object
182 183 184 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 182 def rollback @transaction&.rollback end |
#tables ⇒ Object
186 187 188 189 190 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 186 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 |