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.
154 155 156 157 158 159 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 154 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
173 174 175 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 173 def apply_limit(sql, limit, offset = 0) "SELECT FIRST #{limit} SKIP #{offset} * FROM (#{sql})" end |
#begin_transaction ⇒ Object
177 178 179 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 177 def begin_transaction @transaction = @connection.transaction end |
#close ⇒ Object
127 128 129 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 127 def close @connection&.close end |
#columns(table_name) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 195 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
181 182 183 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 181 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 123 124 125 |
# 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 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
142 143 144 145 146 147 148 149 150 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 142 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
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 131 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
161 162 163 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 161 def last_insert_id nil end |
#placeholder ⇒ Object
165 166 167 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 165 def placeholder "?" end |
#placeholders(count) ⇒ Object
169 170 171 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 169 def placeholders(count) (["?"] * count).join(", ") end |
#rollback ⇒ Object
185 186 187 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 185 def rollback @transaction&.rollback end |
#tables ⇒ Object
189 190 191 192 193 |
# File 'lib/tina4/drivers/firebird_driver.rb', line 189 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 |