diff --git a/lib/mini_sql.rb b/lib/mini_sql.rb index 3b6920f..78d549a 100644 --- a/lib/mini_sql.rb +++ b/lib/mini_sql.rb @@ -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 diff --git a/lib/mini_sql/abstract/prepared_binds.rb b/lib/mini_sql/abstract/prepared_binds.rb index 70a36a5..d432695 100644 --- a/lib/mini_sql/abstract/prepared_binds.rb +++ b/lib/mini_sql/abstract/prepared_binds.rb @@ -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}")] diff --git a/lib/mini_sql/abstract/prepared_cache.rb b/lib/mini_sql/abstract/prepared_cache.rb index c4d713e..a5e2d15 100644 --- a/lib/mini_sql/abstract/prepared_cache.rb +++ b/lib/mini_sql/abstract/prepared_cache.rb @@ -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 @@ -28,6 +28,10 @@ def prepare_statement(sql) private + def raw_connection + @connection.raw_connection + end + def next_key "s#{@counter += 1}" end diff --git a/lib/mini_sql/builder.rb b/lib/mini_sql/builder.rb index 1afae64..135958d 100644 --- a/lib/mini_sql/builder.rb +++ b/lib/mini_sql/builder.rb @@ -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 diff --git a/lib/mini_sql/inline_param_encoder.rb b/lib/mini_sql/inline_param_encoder.rb index 29171eb..3541d5f 100644 --- a/lib/mini_sql/inline_param_encoder.rb +++ b/lib/mini_sql/inline_param_encoder.rb @@ -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 diff --git a/lib/mini_sql/mysql/connection.rb b/lib/mini_sql/mysql/connection.rb index 05b6473..c08b027 100644 --- a/lib/mini_sql/mysql/connection.rb +++ b/lib/mini_sql/mysql/connection.rb @@ -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) diff --git a/lib/mini_sql/mysql/prepared_cache.rb b/lib/mini_sql/mysql/prepared_cache.rb index 8345d21..21e5700 100644 --- a/lib/mini_sql/mysql/prepared_cache.rb +++ b/lib/mini_sql/mysql/prepared_cache.rb @@ -9,7 +9,7 @@ class PreparedCache < ::MiniSql::Abstract::PreparedCache private def alloc(sql) - @connection.prepare(sql) + raw_connection.prepare(sql) end def dealloc(statement) diff --git a/lib/mini_sql/mysql/prepared_connection.rb b/lib/mini_sql/mysql/prepared_connection.rb index 0bd1e64..2efb61d 100644 --- a/lib/mini_sql/mysql/prepared_connection.rb +++ b/lib/mini_sql/mysql/prepared_connection.rb @@ -7,21 +7,15 @@ 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 @@ -29,6 +23,8 @@ def deserializer_cache 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, diff --git a/lib/mini_sql/postgres/connection.rb b/lib/mini_sql/postgres/connection.rb index 38dd2d6..f801f7d 100644 --- a/lib/mini_sql/postgres/connection.rb +++ b/lib/mini_sql/postgres/connection.rb @@ -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 @@ -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 @@ -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. diff --git a/lib/mini_sql/postgres/prepared_binds_auto_array.rb b/lib/mini_sql/postgres/prepared_binds_auto_array.rb new file mode 100644 index 0000000..7bf5df8 --- /dev/null +++ b/lib/mini_sql/postgres/prepared_binds_auto_array.rb @@ -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 diff --git a/lib/mini_sql/postgres/prepared_cache.rb b/lib/mini_sql/postgres/prepared_cache.rb index 86ce56d..ceae8ea 100644 --- a/lib/mini_sql/postgres/prepared_cache.rb +++ b/lib/mini_sql/postgres/prepared_cache.rb @@ -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 diff --git a/lib/mini_sql/postgres/prepared_connection.rb b/lib/mini_sql/postgres/prepared_connection.rb index b69029d..24dc8a6 100644 --- a/lib/mini_sql/postgres/prepared_connection.rb +++ b/lib/mini_sql/postgres/prepared_connection.rb @@ -7,22 +7,16 @@ 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 @@ -30,8 +24,9 @@ def deserializer_cache 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 diff --git a/lib/mini_sql/sqlite/connection.rb b/lib/mini_sql/sqlite/connection.rb index 698f5b2..74f4a0a 100644 --- a/lib/mini_sql/sqlite/connection.rb +++ b/lib/mini_sql/sqlite/connection.rb @@ -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) diff --git a/lib/mini_sql/sqlite/prepared_cache.rb b/lib/mini_sql/sqlite/prepared_cache.rb index f384923..fd38730 100644 --- a/lib/mini_sql/sqlite/prepared_cache.rb +++ b/lib/mini_sql/sqlite/prepared_cache.rb @@ -9,7 +9,7 @@ class PreparedCache < MiniSql::Abstract::PreparedCache private def alloc(sql) - @connection.prepare(sql) + raw_connection.prepare(sql) end def dealloc(statement) diff --git a/lib/mini_sql/sqlite/prepared_connection.rb b/lib/mini_sql/sqlite/prepared_connection.rb index 4b0d9c3..f8ad3f3 100644 --- a/lib/mini_sql/sqlite/prepared_connection.rb +++ b/lib/mini_sql/sqlite/prepared_connection.rb @@ -7,21 +7,15 @@ 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 @@ -29,6 +23,7 @@ def deserializer_cache 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? diff --git a/test/mini_sql/connection_tests.rb b/test/mini_sql/connection_tests.rb index 90f0e2c..45d5d1e 100644 --- a/test/mini_sql/connection_tests.rb +++ b/test/mini_sql/connection_tests.rb @@ -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) diff --git a/test/mini_sql/mysql/prepared_connection_test.rb b/test/mini_sql/mysql/prepared_connection_test.rb index 2cf796b..16fcb47 100644 --- a/test/mini_sql/mysql/prepared_connection_test.rb +++ b/test/mini_sql/mysql/prepared_connection_test.rb @@ -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'") @@ -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 diff --git a/test/mini_sql/postgres/auto_encode_arrays_test.rb b/test/mini_sql/postgres/auto_encode_arrays_test.rb new file mode 100644 index 0000000..e348d93 --- /dev/null +++ b/test/mini_sql/postgres/auto_encode_arrays_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative 'connection_test' +require_relative 'prepared_connection_test' + +module MiniSql::Postgres::ArrayTests + def test_simple_params + nums, strings, empty_array = [1, 2, 3], %w[a b c], [] + + row = @connection.query_single("select ?::int[], ?::text[], ?::int[]", nums, strings, empty_array) + + assert_equal(row, [nums, strings, empty_array]) + end + + def test_hash_params + nums, strings, empty_array = [1, 2, 3], %w[a b c], [] + + row = @connection.query_single("select :nums::int[], :strings::text[], :empty_array::int[]", nums: nums, strings: strings, empty_array: empty_array) + + assert_equal(row, [nums, strings, empty_array]) + end +end + +class MiniSql::Postgres::TestAutoEncodeArraysPrepared < MiniSql::Postgres::TestPreparedConnection + include MiniSql::Postgres::ArrayTests + + def setup + @unprepared_connection = pg_connection(auto_encode_arrays: true) + @connection = @unprepared_connection.prepared + + setup_tables + end +end + +class MiniSql::Postgres::TestAutoEncodeArraysUnprepared < MiniSql::Postgres::TestConnection + include MiniSql::Postgres::ArrayTests + + def setup + @connection = pg_connection(auto_encode_arrays: true) + end +end diff --git a/test/mini_sql/postgres/connection_test.rb b/test/mini_sql/postgres/connection_test.rb index f16ed20..cd63673 100644 --- a/test/mini_sql/postgres/connection_test.rb +++ b/test/mini_sql/postgres/connection_test.rb @@ -155,15 +155,4 @@ def test_unamed_query assert_equal(row.column2, 3) end - def test_encode_array - connection = pg_connection(auto_encode_arrays: true) - - ints = [1, 2, 3] - strings = %w[a b c] - row = connection.query("select ?::int[] ints, ?::text[] strings", ints, strings).first - - assert_equal(row.ints, ints) - assert_equal(row.strings, strings) - end - end diff --git a/test/mini_sql/postgres/prepared_connection_test.rb b/test/mini_sql/postgres/prepared_connection_test.rb index edbe48a..d6125f0 100644 --- a/test/mini_sql/postgres/prepared_connection_test.rb +++ b/test/mini_sql/postgres/prepared_connection_test.rb @@ -7,20 +7,18 @@ class MiniSql::Postgres::TestPreparedConnection < Minitest::Test def setup @unprepared_connection = pg_connection - @prepared_connection = @unprepared_connection.prepared + @connection = @unprepared_connection.prepared - super + setup_tables end def last_prepared_statement - @unprepared_connection.query("select * from pg_prepared_statements")[ - 0 - ]&.statement + @unprepared_connection.query("select * from pg_prepared_statements")[0]&.statement end def test_time r = - @prepared_connection.query( + @connection.query( "select :date::timestamp - '10 days'::interval AS funday", date: Time.parse("2010-10-11T02:22:00Z") ) @@ -31,7 +29,7 @@ def test_time def test_date r = - @prepared_connection.query( + @connection.query( "select :date::date - 10 AS funday", date: Date.parse("2010-10-11") ) @@ -41,7 +39,7 @@ def test_date 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 @@ -50,7 +48,7 @@ def test_boolean_param def test_numbers_param r = - @prepared_connection.query( + @connection.query( "select :price::decimal AS price, :quantity::int AS quantity", price: 20.5, quantity: 3 @@ -62,14 +60,11 @@ def test_numbers_param end def test_limit_prepared_cache - @prepared_connection.instance_variable_get( - :@prepared_cache - ).instance_variable_set(:@max_size, 1) + @connection.instance_variable_set(:@prepared_cache, MiniSql::Postgres::PreparedCache.new(@unprepared_connection, 1)) - assert_equal @prepared_connection.query_single("SELECT ?", 1), %w[1] - assert_equal @prepared_connection.query_single("SELECT ?, ?", 1, 2), %w[1 2] - assert_equal @prepared_connection.query_single("SELECT ?, ?, ?", 1, 2, 3), - %w[1 2 3] + assert_equal @connection.query_single("SELECT ?", 1), %w[1] + assert_equal @connection.query_single("SELECT ?, ?", 1, 2), %w[1 2] + assert_equal @connection.query_single("SELECT ?, ?, ?", 1, 2, 3), %w[1 2 3] ps = @unprepared_connection.query("select * from pg_prepared_statements") assert_equal ps.size, 1 @@ -77,9 +72,10 @@ def test_limit_prepared_cache end def test_single_named_param - r = @prepared_connection.query_single("select :n, :n, :n", n: "test") + r = @connection.query_single("select :n, :n, :n", n: "test") assert_last_stmt "select $1, $1, $1" assert_equal %w[test test test], r end + end diff --git a/test/mini_sql/prepared_connection_tests.rb b/test/mini_sql/prepared_connection_tests.rb index 7b1e0ac..d9589d9 100644 --- a/test/mini_sql/prepared_connection_tests.rb +++ b/test/mini_sql/prepared_connection_tests.rb @@ -2,7 +2,7 @@ module MiniSql::PreparedConnectionTests - def setup + def setup_tables @unprepared_connection.exec "CREATE TEMPORARY table posts(id int, title text, active bool)" @unprepared_connection.exec "INSERT INTO posts(id, title, active) VALUES(1, 'ruby', false), (2, 'super', true), (3, 'language', false)" end @@ -14,7 +14,7 @@ def assert_last_stmt(statement_sql, msg = nil) end def test_disable_prepared - @prepared_connection.prepared(false).exec('select 1') + @connection.unprepared.exec('select 1') assert_nil(last_prepared_statement) end @@ -33,7 +33,9 @@ def test_builder end def test_array_hash_params - r = @prepared_connection.query("SELECT id, title FROM posts WHERE id IN (:ids)", ids: [2, 3]) + return if @unprepared_connection.respond_to?(:array_encoder) && @unprepared_connection.array_encoder + + r = @connection.query("SELECT id, title FROM posts WHERE id IN (:ids)", ids: [2, 3]) assert_last_stmt "SELECT id, title FROM posts WHERE id IN ($1, $2)" assert_equal 2, r[0].id @@ -42,7 +44,9 @@ def test_array_hash_params end def test_array_simple_params - r = @prepared_connection.query("SELECT id, title FROM posts WHERE id IN (?)", [2, 3]) + return if @unprepared_connection.respond_to?(:array_encoder) && @unprepared_connection.array_encoder + + r = @connection.query("SELECT id, title FROM posts WHERE id IN (?)", [2, 3]) assert_last_stmt "SELECT id, title FROM posts WHERE id IN ($1, $2)" assert_equal 2, r[0].id @@ -51,7 +55,7 @@ def test_array_simple_params end def test_string_param - r = @prepared_connection.query_single('SELECT :title', title: 'The ruby') + r = @connection.query_single('SELECT :title', title: 'The ruby') assert_last_stmt "SELECT $1" assert_equal('The ruby', r[0]) diff --git a/test/mini_sql/sqlite/prepared_connection_test.rb b/test/mini_sql/sqlite/prepared_connection_test.rb index 232e9ff..57859a1 100644 --- a/test/mini_sql/sqlite/prepared_connection_test.rb +++ b/test/mini_sql/sqlite/prepared_connection_test.rb @@ -7,9 +7,9 @@ class MiniSql::Sqlite::TestPreparedConnection < Minitest::Test def setup @unprepared_connection = sqlite3_connection - @prepared_connection = @unprepared_connection.prepared + @connection = @unprepared_connection.prepared - super + setup_tables end STMT_SQL = "select * from sqlite_stmt" @@ -26,7 +26,7 @@ def assert_last_stmt(statement_sql) end def test_boolean_param - r = @prepared_connection.query("SELECT * FROM posts WHERE active = ?", 1) + r = @connection.query("SELECT * FROM posts WHERE active = ?", 1) assert_last_stmt "SELECT * FROM posts WHERE active = $1" assert_equal 2, r[0].id