Class: PgSqlCaller::Model
- Inherits:
-
Object
- Object
- PgSqlCaller::Model
- Defined in:
- lib/pg_sql_caller/model.rb
Overview
Wraps a single ActiveRecord class and runs raw SQL through its connection. Positional ‘?` placeholders are bound and sanitized by ActiveRecord, so values are never interpolated into the SQL string.
sql = PgSqlCaller::Model.new(ApplicationRecord)
sql.select_value('SELECT count(*) FROM users WHERE active = ?', true) # => 42
sql.select_values('SELECT email FROM users WHERE dept_id = ?', 5) # => ['a@x', 'b@x']
sql.select_all('SELECT id, name FROM users') # => [{ 'id' => 1, 'name' => 'Jo' }, ...]
sql.transaction { sql.execute('UPDATE users SET active = false') }
The ‘*_serialized` variants additionally cast each value back to its Ruby type using the result’s column types (e.g. timestamp -> Time, int[] -> Array), and key rows by Symbol:
sql.select_all_serialized('SELECT id, created_at FROM users')
# => [{ id: 1, created_at: 2026-06-08 12:00:00 +0000 }, ...]
Direct Known Subclasses
Instance Attribute Summary collapse
-
#model_class ⇒ Class<ActiveRecord::Base>
readonly
The ActiveRecord class this instance wraps.
Class Method Summary collapse
-
.define_sql_method(name) ⇒ Symbol
Define a single connection-backed SQL instance method named
name. -
.define_sql_methods(*names) ⇒ Array<Symbol>
Define several connection-backed SQL instance methods at once — a thin wrapper over Model.define_sql_method, kept for backward compatibility.
Instance Method Summary collapse
-
#connection ⇒ ActiveRecord::ConnectionAdapters::AbstractAdapter
The ActiveRecord connection adapter of #model_class; every SQL method runs through it.
-
#current_database ⇒ String
The name of the currently connected database (‘current_database()`).
-
#execute(sql, *bindings) ⇒ PG::Result
Execute
sql(e.g. INSERT/UPDATE/DELETE/DDL) and return the raw adapter result. -
#explain_analyze(sql) ⇒ String
Run ‘EXPLAIN ANALYZE` for
sqland return the query plan as text. -
#initialize(model_class) ⇒ Model
constructor
A new instance of Model.
-
#next_sequence_value(table_name) ⇒ Integer
The next value of the table’s ‘<table_name>_id_seq` sequence (its current last_value + 1), read without consuming the sequence.
-
#quote_column_name(name) ⇒ String
Quote a column-name identifier for safe inclusion in SQL (delegated to the #connection, since the model class itself does not expose it).
-
#quote_table_name(name) ⇒ String
Quote a table-name identifier for safe inclusion in SQL (delegated to the #connection, since the model class itself does not expose it).
-
#quote_value(value) ⇒ String
Quote and escape a value as a SQL literal, safe to inline into a statement.
-
#sanitize_sql_array(sql, *bindings) ⇒ String
Interpolate ‘?` placeholders in
sqlwithbindingsthrough ActiveRecord’s sanitizer (values are quoted/escaped, never raw-interpolated). -
#select_all(sql, *bindings) ⇒ ActiveRecord::Result
Run
sqland return every row. -
#select_all_serialized(sql, *bindings) ⇒ Array<Hash{Symbol => Object}>
Like #select_all, but cast each value back to its Ruby type (using the result’s column types) and key every row by Symbol.
-
#select_row(sql, *bindings) ⇒ Array?
Run
sqland return the first row as an array of column values. -
#select_rows(sql, *bindings) ⇒ Array<Array>
Run
sqland return rows as arrays of column values (no column names). -
#select_value(sql, *bindings) ⇒ Object?
Run
sqland return the value of the first column of the first row. -
#select_value_serialized(sql, *bindings) ⇒ Object?
Like #select_value, but cast the value back to its Ruby type.
-
#select_values(sql, *bindings) ⇒ Array<Object>
Run
sqland return the first column of every row. -
#select_values_serialized(sql, *bindings) ⇒ Array<Array>
Run
sqland return each row as an array of its type-cast column values. -
#table_data_size(table_name) ⇒ Integer
On-disk size of the table’s main data fork only, in bytes (PostgreSQL ‘pg_relation_size`).
-
#table_full_size(table_name) ⇒ Integer
Total on-disk size of the table including indexes and TOAST, in bytes (PostgreSQL ‘pg_total_relation_size`).
-
#transaction { ... } ⇒ Object
Run the given block inside a database transaction, committing on success and rolling back if it raises.
-
#transaction_open? ⇒ Boolean
Whether a database transaction is currently open on the connection.
-
#typecast_array(values, type:) ⇒ String
Encode a Ruby array into a PostgreSQL array literal for the given attribute type, ready to bind as a single ‘?` value.
-
#with_min_messages(level) { ... } ⇒ Object
Temporarily set the connection’s
client_min_messagestolevelfor the duration of the block, restoring the previous value afterward. -
#with_notice_processor(callback) { ... } ⇒ Object
Capture PostgreSQL NOTICE output (e.g. from RAISE NOTICE) emitted while the block runs, passing each message to
callback.
Constructor Details
#initialize(model_class) ⇒ Model
Returns a new instance of Model.
105 106 107 |
# File 'lib/pg_sql_caller/model.rb', line 105 def initialize(model_class) @model_class = model_class end |
Instance Attribute Details
#model_class ⇒ Class<ActiveRecord::Base> (readonly)
Returns the ActiveRecord class this instance wraps.
82 83 84 |
# File 'lib/pg_sql_caller/model.rb', line 82 def model_class @model_class end |
Class Method Details
.define_sql_method(name) ⇒ Symbol
Define a single connection-backed SQL instance method named name.
29 30 31 32 33 34 |
# File 'lib/pg_sql_caller/model.rb', line 29 def define_sql_method(name) define_method(name) do |sql, *bindings| sql = sanitize_sql_array(sql, *bindings) if bindings.any? connection.public_send(name, sql) end end |
.define_sql_methods(*names) ⇒ Array<Symbol>
Define several connection-backed SQL instance methods at once — a thin wrapper over define_sql_method, kept for backward compatibility.
41 42 43 |
# File 'lib/pg_sql_caller/model.rb', line 41 def define_sql_methods(*names) names.each { |name| define_sql_method(name) } end |
Instance Method Details
#connection ⇒ ActiveRecord::ConnectionAdapters::AbstractAdapter
The ActiveRecord connection adapter of #model_class; every SQL method runs through it.
87 |
# File 'lib/pg_sql_caller/model.rb', line 87 delegate :connection, to: :model_class |
#current_database ⇒ String
Returns the name of the currently connected database (‘current_database()`).
242 243 244 |
# File 'lib/pg_sql_caller/model.rb', line 242 def current_database select_value('SELECT current_database();') end |
#execute(sql, *bindings) ⇒ PG::Result
Execute sql (e.g. INSERT/UPDATE/DELETE/DDL) and return the raw adapter result.
65 |
# File 'lib/pg_sql_caller/model.rb', line 65 define_sql_method :execute |
#explain_analyze(sql) ⇒ String
Run ‘EXPLAIN ANALYZE` for sql and return the query plan as text.
211 212 213 214 215 216 217 |
# File 'lib/pg_sql_caller/model.rb', line 211 def explain_analyze(sql) # `sql` is the statement to analyze; the caller owns the full SQL by contract (like #execute), # so there is no boundary to bind across. # nosemgrep: pg-sql-caller-interpolated-raw-sql result = select_values("EXPLAIN ANALYZE #{sql}") ['QUERY_PLAN', *result].join("\n") end |
#next_sequence_value(table_name) ⇒ Integer
The next value of the table’s ‘<table_name>_id_seq` sequence (its current last_value + 1), read without consuming the sequence.
160 161 162 163 164 165 166 |
# File 'lib/pg_sql_caller/model.rb', line 160 def next_sequence_value(table_name) sequence_name = quote_table_name("#{table_name}_id_seq") # `sequence_name` is an identifier escaped via quote_table_name (identifiers cannot use `?` # bindings), so the interpolation below is injection-safe. # nosemgrep: pg-sql-caller-interpolated-raw-sql select_value("SELECT last_value FROM #{sequence_name}") + 1 end |
#quote_column_name(name) ⇒ String
Quote a column-name identifier for safe inclusion in SQL (delegated to the #connection, since the model class itself does not expose it).
94 |
# File 'lib/pg_sql_caller/model.rb', line 94 delegate :quote_column_name, to: :connection |
#quote_table_name(name) ⇒ String
Quote a table-name identifier for safe inclusion in SQL (delegated to the #connection, since the model class itself does not expose it).
101 |
# File 'lib/pg_sql_caller/model.rb', line 101 delegate :quote_table_name, to: :connection |
#quote_value(value) ⇒ String
Quote and escape a value as a SQL literal, safe to inline into a statement.
285 286 287 |
# File 'lib/pg_sql_caller/model.rb', line 285 def quote_value(value) connection.quote(value) end |
#sanitize_sql_array(sql, *bindings) ⇒ String
Interpolate ‘?` placeholders in sql with bindings through ActiveRecord’s sanitizer (values are quoted/escaped, never raw-interpolated).
237 238 239 |
# File 'lib/pg_sql_caller/model.rb', line 237 def sanitize_sql_array(sql, *bindings) model_class.send :sanitize_sql_array, bindings.unshift(sql) end |
#select_all(sql, *bindings) ⇒ ActiveRecord::Result
Run sql and return every row.
72 |
# File 'lib/pg_sql_caller/model.rb', line 72 define_sql_method :select_all |
#select_all_serialized(sql, *bindings) ⇒ Array<Hash{Symbol => Object}>
Like #select_all, but cast each value back to its Ruby type (using the result’s column types) and key every row by Symbol.
122 123 124 125 126 127 |
# File 'lib/pg_sql_caller/model.rb', line 122 def select_all_serialized(sql, *bindings) result = select_all(sql, *bindings) result.map do |row| row.to_h { |key, value| [key.to_sym, deserialize_result(result, key, value)] } end end |
#select_row(sql, *bindings) ⇒ Array?
Run sql and return the first row as an array of column values.
191 192 193 |
# File 'lib/pg_sql_caller/model.rb', line 191 def select_row(sql, *bindings) select_rows(sql, *bindings)[0] end |
#select_rows(sql, *bindings) ⇒ Array<Array>
Run sql and return rows as arrays of column values (no column names).
79 |
# File 'lib/pg_sql_caller/model.rb', line 79 define_sql_method :select_rows |
#select_value(sql, *bindings) ⇒ Object?
Run sql and return the value of the first column of the first row.
51 |
# File 'lib/pg_sql_caller/model.rb', line 51 define_sql_method :select_value |
#select_value_serialized(sql, *bindings) ⇒ Object?
Like #select_value, but cast the value back to its Ruby type.
134 135 136 137 138 139 140 141 |
# File 'lib/pg_sql_caller/model.rb', line 134 def select_value_serialized(sql, *bindings) result = select_all(sql, *bindings) key = result.first&.keys&.first return if key.nil? value = result.first.values.first deserialize_result(result, key, value) end |
#select_values(sql, *bindings) ⇒ Array<Object>
Run sql and return the first column of every row.
58 |
# File 'lib/pg_sql_caller/model.rb', line 58 define_sql_method :select_values |
#select_values_serialized(sql, *bindings) ⇒ Array<Array>
Run sql and return each row as an array of its type-cast column values.
148 149 150 151 152 153 |
# File 'lib/pg_sql_caller/model.rb', line 148 def select_values_serialized(sql, *bindings) result = select_all(sql, *bindings) result.map do |row| row.map { |key, value| deserialize_result(result, key, value) } end end |
#table_data_size(table_name) ⇒ Integer
On-disk size of the table’s main data fork only, in bytes (PostgreSQL ‘pg_relation_size`).
182 183 184 |
# File 'lib/pg_sql_caller/model.rb', line 182 def table_data_size(table_name) select_value('SELECT pg_relation_size(?)', table_name) end |
#table_full_size(table_name) ⇒ Integer
Total on-disk size of the table including indexes and TOAST, in bytes (PostgreSQL ‘pg_total_relation_size`).
173 174 175 |
# File 'lib/pg_sql_caller/model.rb', line 173 def table_full_size(table_name) select_value('SELECT pg_total_relation_size(?)', table_name) end |
#transaction { ... } ⇒ Object
Run the given block inside a database transaction, committing on success and rolling back if it raises.
201 202 203 204 205 |
# File 'lib/pg_sql_caller/model.rb', line 201 def transaction(&) raise ArgumentError, 'block must be given' unless block_given? connection.transaction(&) end |
#transaction_open? ⇒ Boolean
Whether a database transaction is currently open on the connection.
112 113 114 |
# File 'lib/pg_sql_caller/model.rb', line 112 def transaction_open? connection.send(:transaction_open?) end |
#typecast_array(values, type:) ⇒ String
Encode a Ruby array into a PostgreSQL array literal for the given attribute type, ready to bind as a single ‘?` value.
225 226 227 228 229 |
# File 'lib/pg_sql_caller/model.rb', line 225 def typecast_array(values, type:) type = ActiveRecord::Type.lookup(type, array: true) data = type.serialize(values) data.encoder.encode(data.values) end |
#with_min_messages(level) { ... } ⇒ Object
Temporarily set the connection’s client_min_messages to level for the duration of the block, restoring the previous value afterward.
273 274 275 276 277 278 279 |
# File 'lib/pg_sql_caller/model.rb', line 273 def (level) old_level = select_value('SHOW client_min_messages') execute('SET client_min_messages TO ?', level) yield ensure execute('SET client_min_messages TO ?', old_level) unless old_level.nil? end |
#with_notice_processor(callback) { ... } ⇒ Object
Capture PostgreSQL NOTICE output (e.g. from RAISE NOTICE) emitted while the block runs, passing each message to callback. Lowers client_min_messages to notice for the duration (see #with_min_messages) and restores the previous notice processor afterward.
sql.with_notice_processor(->(msg) { logger.info(msg) }) do
sql.execute("DO $$ BEGIN RAISE NOTICE 'hi'; END $$")
end
258 259 260 261 262 263 264 265 |
# File 'lib/pg_sql_caller/model.rb', line 258 def with_notice_processor(callback) ('notice') do old_processor = connection.raw_connection.set_notice_processor { |result| callback.call(result.to_s.chomp) } yield ensure connection.raw_connection.set_notice_processor(&old_processor) end end |