Skip to content

Commit

Permalink
[prepared statements] fix for auto_encode_arrays, refactoring (#59)
Browse files Browse the repository at this point in the history
* [auto_encode_arrays, perf] fix empty array

* fix prepared statements with auto_encode_arrays

* refactor PreparedConnection for more useful ActiveRecord

* test refactoring

* simplify prepared method
  • Loading branch information
ermolaev authored Sep 4, 2024
1 parent 253f913 commit 3766913
Show file tree
Hide file tree
Showing 22 changed files with 183 additions and 110 deletions.
1 change: 1 addition & 0 deletions lib/mini_sql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module Postgres
autoload :PreparedConnection, "mini_sql/postgres/prepared_connection"
autoload :PreparedCache, "mini_sql/postgres/prepared_cache"
autoload :PreparedBinds, "mini_sql/postgres/prepared_binds"
autoload :PreparedBindsAutoArray, "mini_sql/postgres/prepared_binds_auto_array"
end

module ActiveRecordPostgres
Expand Down
5 changes: 2 additions & 3 deletions lib/mini_sql/abstract/prepared_binds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,12 @@ def bind_hash(sql, hash)

def bind_array(sql, array)
sql = sql.dup
param_i = 0
param_i = -1
i = 0
binds = []
bind_names = []
sql.gsub!("?") do
param_i += 1
array_wrap(array[param_i - 1]).map do |vv|
array_wrap(array[param_i += 1]).map do |vv|
binds << vv
i += 1
bind_names << [BindName.new("$#{i}")]
Expand Down
6 changes: 5 additions & 1 deletion lib/mini_sql/abstract/prepared_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize(connection, max_size = nil)
end

def prepare_statement(sql)
stm_key = "#{@connection.object_id}-#{sql}"
stm_key = "#{raw_connection.object_id}-#{sql}"
statement = @cache.delete(stm_key)
if statement
@cache[stm_key] = statement
Expand All @@ -28,6 +28,10 @@ def prepare_statement(sql)

private

def raw_connection
@connection.raw_connection
end

def next_key
"s#{@counter += 1}"
end
Expand Down
5 changes: 2 additions & 3 deletions lib/mini_sql/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ def query_decorator(decorator, hash_args = nil)
connection_switcher.query_decorator(decorator, parametrized_sql, union_parameters(hash_args))
end

def prepared(condition = true)
@is_prepared = condition

def prepared
@is_prepared = true
self
end

Expand Down
24 changes: 13 additions & 11 deletions lib/mini_sql/inline_param_encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,21 @@ def quoted_time(value)
value.utc.iso8601
end

EMPTY_ARRAY = [].freeze

def quote_val(value)
case value
when String then "'#{conn.escape_string(value.to_s)}'"
when Numeric then value.to_s
when BigDecimal then value.to_s("F")
when Time then "'#{quoted_time(value)}'"
when Date then "'#{value.to_s}'"
when Symbol then "'#{conn.escape_string(value.to_s)}'"
when true then "true"
when false then "false"
when nil then "NULL"
when [] then "NULL"
when Array then array_encoder ? "'#{array_encoder.encode(value)}'" : value.map { |v| quote_val(v) }.join(', ')
when String then "'#{conn.escape_string(value.to_s)}'"
when Numeric then value.to_s
when BigDecimal then value.to_s("F")
when Time then "'#{quoted_time(value)}'"
when Date then "'#{value.to_s}'"
when Symbol then "'#{conn.escape_string(value.to_s)}'"
when true then "true"
when false then "false"
when nil then "NULL"
when EMPTY_ARRAY then array_encoder ? "'{}'" : "NULL"
when Array then array_encoder ? "'#{array_encoder.encode(value)}'" : value.map { |v| quote_val(v) }.join(', ')
else raise TypeError, "can't quote #{value.class.name}"
end
end
Expand Down
8 changes: 2 additions & 6 deletions lib/mini_sql/mysql/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@ def initialize(raw_connection, args = nil)
@deserializer_cache = (args && args[:deserializer_cache]) || DeserializerCache.new
end

def prepared(condition = true)
if condition
@prepared ||= PreparedConnection.new(self)
else
self
end
def prepared
@prepared ||= PreparedConnection.new(self)
end

def query_single(sql, *params)
Expand Down
2 changes: 1 addition & 1 deletion lib/mini_sql/mysql/prepared_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class PreparedCache < ::MiniSql::Abstract::PreparedCache
private

def alloc(sql)
@connection.prepare(sql)
raw_connection.prepare(sql)
end

def dealloc(statement)
Expand Down
14 changes: 5 additions & 9 deletions lib/mini_sql/mysql/prepared_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,24 @@ class PreparedConnection < Connection
attr_reader :unprepared

def initialize(unprepared_connection)
@unprepared = unprepared_connection
@raw_connection = unprepared_connection.raw_connection
@param_encoder = unprepared_connection.param_encoder

@prepared_cache = PreparedCache.new(@raw_connection)
@param_binder = PreparedBinds.new
@unprepared = unprepared_connection
@param_binder = PreparedBinds.new
end

def build(_)
raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
end

def prepared(condition = true)
condition ? self : @unprepared
end
undef_method :prepared

def deserializer_cache
@unprepared.deserializer_cache
end

private def run(sql, as, params)
prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)

@prepared_cache ||= PreparedCache.new(unprepared)
statement = @prepared_cache.prepare_statement(prepared_sql)
statement.execute(
*binds,
Expand Down
12 changes: 4 additions & 8 deletions lib/mini_sql/postgres/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module MiniSql
module Postgres
class Connection < MiniSql::Connection
attr_reader :raw_connection, :param_encoder, :deserializer_cache
attr_reader :raw_connection, :param_encoder, :deserializer_cache, :array_encoder

def self.default_deserializer_cache
@deserializer_cache ||= DeserializerCache.new
Expand Down Expand Up @@ -52,7 +52,7 @@ def self.type_map(conn)
def initialize(raw_connection, args = nil)
@raw_connection = raw_connection
@deserializer_cache = (args && args[:deserializer_cache]) || self.class.default_deserializer_cache
array_encoder = PG::TextEncoder::Array.new if args && args[:auto_encode_arrays]
@array_encoder = PG::TextEncoder::Array.new if args && args[:auto_encode_arrays]
@param_encoder = (args && args[:param_encoder]) || InlineParamEncoder.new(self, array_encoder)
@type_map = args && args[:type_map]
end
Expand All @@ -61,12 +61,8 @@ def type_map
@type_map ||= self.class.type_map(raw_connection)
end

def prepared(condition = true)
if condition
@prepared ||= PreparedConnection.new(self)
else
self
end
def prepared
@prepared ||= PreparedConnection.new(self)
end

# Returns a flat array containing all results.
Expand Down
61 changes: 61 additions & 0 deletions lib/mini_sql/postgres/prepared_binds_auto_array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require "mini_sql/abstract/prepared_binds"

module MiniSql
module Postgres
class PreparedBindsAutoArray < ::MiniSql::Abstract::PreparedBinds

attr_reader :array_encoder

def initialize(array_encoder)
@array_encoder = array_encoder
end

def bind_hash(sql, hash)
sql = sql.dup
binds = []
bind_names = []
i = 0

hash.each do |k, v|
binds << (v.is_a?(Array) ? array_encoder.encode(v) : v)
bind_names << [BindName.new(k)]
bind_outputs = bind_output(i += 1)

sql.gsub!(":#{k}") do
# ignore ::int and stuff like that
# $` is previous to match
if $` && $`[-1] != ":"
bind_outputs
else
":#{k}"
end
end
end
[sql, binds, bind_names]
end

def bind_array(sql, array)
sql = sql.dup
param_i = -1
i = 0
binds = []
bind_names = []
sql.gsub!("?") do
v = array[param_i += 1]
binds << (v.is_a?(Array) ? array_encoder.encode(v) : v)
i += 1
bind_names << [BindName.new("$#{i}")]
bind_output(i)
end
[sql, binds, bind_names]
end

def bind_output(i)
"$#{i}"
end

end
end
end
4 changes: 2 additions & 2 deletions lib/mini_sql/postgres/prepared_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ class PreparedCache < ::MiniSql::Abstract::PreparedCache

def alloc(sql)
alloc_key = next_key
@connection.prepare(alloc_key, sql)
raw_connection.prepare(alloc_key, sql)

alloc_key
end

def dealloc(key)
@connection.query "DEALLOCATE #{key}" if @connection.status == PG::CONNECTION_OK
raw_connection.query "DEALLOCATE #{key}" if raw_connection.status == PG::CONNECTION_OK
rescue PG::Error
end

Expand Down
17 changes: 6 additions & 11 deletions lib/mini_sql/postgres/prepared_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,26 @@ class PreparedConnection < Connection
attr_reader :unprepared

def initialize(unprepared_connection)
@unprepared = unprepared_connection
@raw_connection = unprepared_connection.raw_connection
@type_map = unprepared_connection.type_map
@param_encoder = unprepared_connection.param_encoder

@prepared_cache = PreparedCache.new(@raw_connection)
@param_binder = PreparedBinds.new
@unprepared = unprepared_connection
@type_map = unprepared_connection.type_map
@param_binder = unprepared.array_encoder ? PreparedBindsAutoArray.new(unprepared.array_encoder) : PreparedBinds.new
end

def build(_)
raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
end

def prepared(condition = true)
condition ? self : @unprepared
end
undef_method :prepared

def deserializer_cache
@unprepared.deserializer_cache
end

private def run(sql, params)
prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
@prepared_cache ||= PreparedCache.new(unprepared)
prepare_statement_key = @prepared_cache.prepare_statement(prepared_sql)
raw_connection.exec_prepared(prepare_statement_key, binds)
unprepared.raw_connection.exec_prepared(prepare_statement_key, binds)
end

end
Expand Down
8 changes: 2 additions & 6 deletions lib/mini_sql/sqlite/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@ def initialize(raw_connection, args = nil)
@deserializer_cache = (args && args[:deserializer_cache]) || DeserializerCache.new
end

def prepared(condition = true)
if condition
@prepared ||= PreparedConnection.new(self)
else
self
end
def prepared
@prepared ||= PreparedConnection.new(self)
end

def query_single(sql, *params)
Expand Down
2 changes: 1 addition & 1 deletion lib/mini_sql/sqlite/prepared_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class PreparedCache < MiniSql::Abstract::PreparedCache
private

def alloc(sql)
@connection.prepare(sql)
raw_connection.prepare(sql)
end

def dealloc(statement)
Expand Down
13 changes: 4 additions & 9 deletions lib/mini_sql/sqlite/prepared_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,23 @@ class PreparedConnection < Connection
attr_reader :unprepared

def initialize(unprepared_connection)
@unprepared = unprepared_connection
@raw_connection = unprepared_connection.raw_connection
@param_encoder = unprepared_connection.param_encoder

@prepared_cache = PreparedCache.new(@raw_connection)
@param_binder = PreparedBinds.new
@unprepared = unprepared_connection
@param_binder = PreparedBinds.new
end

def build(_)
raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
end

def prepared(condition = true)
condition ? self : @unprepared
end
undef_method :prepared

def deserializer_cache
@unprepared.deserializer_cache
end

private def run(sql, params)
prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
@prepared_cache ||= PreparedCache.new(unprepared)
statement = @prepared_cache.prepare_statement(prepared_sql)
statement.bind_params(binds)
if block_given?
Expand Down
2 changes: 2 additions & 0 deletions test/mini_sql/connection_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def test_can_query_single_multi
end

def test_can_deal_with_arrays
return if @connection.respond_to?(:array_encoder) && @connection.array_encoder

r = @connection.query_single("select :array as array", array: [1, 2, 3])
assert_equal([1, 2, 3], r)

Expand Down
6 changes: 3 additions & 3 deletions test/mini_sql/mysql/prepared_connection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class MiniSql::Mysql::TestPreparedConnection < Minitest::Test

def setup
@unprepared_connection = mysql_connection
@prepared_connection = @unprepared_connection.prepared
@connection = @unprepared_connection.prepared

super
setup_tables

@unprepared_connection.exec("SET GLOBAL log_output = 'TABLE'")
@unprepared_connection.exec("SET GLOBAL general_log = 'ON'")
Expand All @@ -28,7 +28,7 @@ def assert_last_stmt(statement_sql)
end

def test_boolean_param
r = @prepared_connection.query("SELECT * FROM posts WHERE active = ?", true)
r = @connection.query("SELECT * FROM posts WHERE active = ?", true)

assert_last_stmt "SELECT * FROM posts WHERE active = $1"
assert_equal 2, r[0].id
Expand Down
Loading

0 comments on commit 3766913

Please sign in to comment.