Class: Tina4::Drivers::PostgresDriver
- Inherits:
-
Object
- Object
- Tina4::Drivers::PostgresDriver
- Includes:
- SchemaSplit
- Defined in:
- lib/tina4/drivers/postgres_driver.rb
Instance Attribute Summary collapse
-
#connection ⇒ Object
readonly
Returns the value of attribute connection.
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
-
#insert(table, data) ⇒ Object
Issue #256: surface the ACTUAL primary key value an INSERT wrote — including a server-generated UUID — instead of guessing it from a session sequence after the fact.
- #last_insert_id ⇒ Object
- #placeholder ⇒ Object
- #placeholders(count) ⇒ Object
- #rollback ⇒ Object
-
#table_exists?(name) ⇒ Boolean
v3.13.14 (#48): to_regclass resolves a (possibly schema-qualified) relation name and search_path like a FROM clause; nil if absent.
- #tables ⇒ Object
Methods included from SchemaSplit
Instance Attribute Details
#connection ⇒ Object (readonly)
Returns the value of attribute connection.
9 10 11 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 9 def connection @connection end |
Instance Method Details
#apply_limit(sql, limit, offset = 0) ⇒ Object
155 156 157 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 155 def apply_limit(sql, limit, offset = 0) "#{sql} LIMIT #{limit} OFFSET #{offset}" end |
#begin_transaction ⇒ Object
159 160 161 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 159 def begin_transaction @connection.exec("BEGIN") end |
#close ⇒ Object
32 33 34 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 32 def close @connection&.close end |
#columns(table_name) ⇒ Object
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 188 def columns(table_name) # v3.13.14 (#48): honour a schema-qualified name; default to public. schema, tbl = split_schema(table_name) schema ||= "public" sql = "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1 AND table_schema = $2" rows = execute_query(sql, [tbl, schema]) rows.map do |r| { name: r[:column_name], type: r[:data_type], nullable: r[:is_nullable] == "YES", default: r[:column_default], primary_key: false } end end |
#commit ⇒ Object
163 164 165 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 163 def commit @connection.exec("COMMIT") end |
#connect(connection_string, username: nil, password: nil) ⇒ Object
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 11 def connect(connection_string, username: nil, password: nil) begin require "pg" rescue LoadError raise LoadError, "The 'pg' gem is required for PostgreSQL connections. Install one of:\n" \ " bundle add pg # if your project uses Bundler\n" \ " gem install pg # bare driver" end url = connection_string if username || password uri = URI.parse(url) uri.user = username if username uri.password = password if password url = uri.to_s end @connection = PG.connect(url) apply_result_type_map(@connection) @connection end |
#execute(sql, params = []) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 46 def execute(sql, params = []) # Issue #256: a bare INSERT run through execute() (not #insert, so no # RETURNING captured) must NOT let a previously-captured RETURNING id # leak into a later last_insert_id() — that would surface a stale id # (e.g. a UUID string from an earlier db.insert) for this new write. # Clear the cache so last_insert_id falls back to the lastval() probe, # which is the correct source for a sequence-backed bare INSERT. @last_returning_id = nil if sql.lstrip[0, 6].upcase == "INSERT" converted_sql = convert_placeholders(sql) if params.empty? @connection.exec(converted_sql) else @connection.exec_params(converted_sql, params) end end |
#execute_query(sql, params = []) ⇒ Object
36 37 38 39 40 41 42 43 44 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 36 def execute_query(sql, params = []) converted_sql = convert_placeholders(sql) result = if params.empty? @connection.exec(converted_sql) else @connection.exec_params(converted_sql, params) end result.map { |row| decode_blobs(symbolize_keys(row)) } end |
#insert(table, data) ⇒ Object
Issue #256: surface the ACTUAL primary key value an INSERT wrote —including a server-generated UUID — instead of guessing it from a session sequence after the fact.
Before this, Database#insert ran a bare INSERT and then probed “last_insert_id“ (“SELECT lastval()“). For a UUID PK (“id uuid PRIMARY KEY DEFAULT gen_random_uuid()“) there is no session sequence, so the probe returned nil — or, worse, a STALE integer left over from an unrelated SERIAL table’s nextval() earlier in the same session (a silently WRONG id). The SERIAL integer path was correct only by luck of lastval() pointing at the right sequence.
Fix (mirrors the Python master’s “INSERT … RETURNING *“ and the Node adapter): append “RETURNING *“ and read the generated “id“ back from the returned row. The value is normalised so the SERIAL path keeps returning an Integer while a UUID PK surfaces its real 36-char string. No lastval() probe, so the issue-#38 transaction-abort can’t happen on this path at all.
Returns { success: true, last_id: <id-or-nil> }. last_id is nil only when the table truly has no “id“ column.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 83 def insert(table, data) columns = data.keys.map(&:to_s) placeholders = placeholders(columns.length) sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES (#{placeholders}) RETURNING *" result = execute_query(sql, data.values) row = result.is_a?(Array) ? result.first : nil id = normalize_returned_id(row) # Remember the real id so a follow-up #last_insert_id / db.get_last_id # surfaces THIS value (incl. a UUID string) instead of re-probing # lastval(), which has no sequence for a UUID PK and would return a # stale wrong integer from an unrelated table. @last_returning_id = id { success: true, last_id: id } end |
#last_insert_id ⇒ Object
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 98 def last_insert_id # Issue #256: if the most recent write surfaced its real primary key # through ``RETURNING *`` (the #insert path), return that — it is the # actual id written (a UUID string stays a string, a SERIAL stays an # integer), not a guess. Only fall back to the lastval() probe below # when nothing has been captured yet (e.g. a bare # ``execute("INSERT ...")`` with no RETURNING). return @last_returning_id unless @last_returning_id.nil? # Issue #38: ``SELECT lastval()`` raises on tables with no sequence # (UUID, ULID, hash PKs etc.). The exception itself isn't fatal, # but the pg gem marks the whole transaction as aborted, so every # subsequent statement on this connection fails with # ``PG::InFailedSqlTransaction`` — far away from the real cause. # # Fix: wrap the probe in a SAVEPOINT. If ``lastval()`` raises, we # ROLLBACK TO SAVEPOINT and the outer transaction stays usable; # ``last_insert_id`` just returns ``nil`` (same as before for # tables without a sequence). On success we RELEASE SAVEPOINT. begin @connection.exec("SAVEPOINT _t4_lastval_probe") rescue PG::Error # No active transaction (autocommit/idle) — fall back to a plain # probe; psycopg2-style transaction abort can't happen here. begin result = @connection.exec("SELECT lastval()") return result.first["lastval"].to_i rescue PG::Error return nil end end begin result = @connection.exec("SELECT lastval()") @connection.exec("RELEASE SAVEPOINT _t4_lastval_probe") result.first["lastval"].to_i rescue PG::Error begin @connection.exec("ROLLBACK TO SAVEPOINT _t4_lastval_probe") @connection.exec("RELEASE SAVEPOINT _t4_lastval_probe") rescue PG::Error # If even the rollback fails, there's nothing we can do — the # connection is in a state we can't recover. Surface nil so # callers don't get a half-set last_id. end nil end end |
#placeholder ⇒ Object
147 148 149 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 147 def placeholder "?" end |
#placeholders(count) ⇒ Object
151 152 153 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 151 def placeholders(count) (1..count).map { |i| "$#{i}" }.join(", ") end |
#rollback ⇒ Object
167 168 169 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 167 def rollback @connection.exec("ROLLBACK") end |
#table_exists?(name) ⇒ Boolean
v3.13.14 (#48): to_regclass resolves a (possibly schema-qualified) relation name and search_path like a FROM clause; nil if absent.
173 174 175 176 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 173 def table_exists?(name) rows = execute_query("SELECT to_regclass($1) AS oid", [name.to_s]) !rows.empty? && !rows[0][:oid].nil? end |
#tables ⇒ Object
178 179 180 181 182 183 184 185 186 |
# File 'lib/tina4/drivers/postgres_driver.rb', line 178 def tables # v3.13.14 (#48): list every user schema; public tables stay bare, # others are returned schema-qualified. sql = "SELECT schemaname, tablename FROM pg_tables " \ "WHERE schemaname NOT IN ('pg_catalog', 'information_schema') " \ "ORDER BY schemaname, tablename" rows = execute_query(sql) rows.map { |r| r[:schemaname] == "public" ? r[:tablename] : "#{r[:schemaname]}.#{r[:tablename]}" } end |