mini_sql 0.2.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +66 -0
- data/.rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml +355 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG +22 -0
- data/Gemfile +3 -1
- data/Guardfile +2 -0
- data/README.md +125 -1
- data/Rakefile +3 -1
- data/bench/builder_perf.rb +138 -0
- data/bench/decorator_perf.rb +143 -0
- data/bench/mini_sql_methods_perf.rb +80 -0
- data/bench/prepared_perf.rb +59 -0
- data/bench/shared/generate_data.rb +133 -0
- data/bench/timestamp_perf.rb +22 -21
- data/bench/topic_mysql_perf.rb +1 -7
- data/bench/topic_perf.rb +27 -169
- data/bench/topic_wide_perf.rb +92 -0
- data/bin/console +1 -0
- data/lib/mini_sql.rb +20 -8
- data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
- data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
- data/lib/mini_sql/builder.rb +64 -24
- data/lib/mini_sql/connection.rb +15 -3
- data/lib/mini_sql/decoratable.rb +22 -0
- data/lib/mini_sql/deserializer_cache.rb +2 -0
- data/lib/mini_sql/inline_param_encoder.rb +12 -13
- data/lib/mini_sql/mysql/connection.rb +18 -3
- data/lib/mini_sql/mysql/deserializer_cache.rb +14 -16
- data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
- data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
- data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
- data/lib/mini_sql/postgres/coders.rb +2 -0
- data/lib/mini_sql/postgres/connection.rb +89 -0
- data/lib/mini_sql/postgres/deserializer_cache.rb +36 -16
- data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
- data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
- data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
- data/lib/mini_sql/postgres_jdbc/connection.rb +8 -1
- data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +43 -43
- data/lib/mini_sql/result.rb +30 -0
- data/lib/mini_sql/serializer.rb +84 -0
- data/lib/mini_sql/sqlite/connection.rb +20 -2
- data/lib/mini_sql/sqlite/deserializer_cache.rb +14 -16
- data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
- data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
- data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
- data/lib/mini_sql/version.rb +1 -1
- data/mini_sql.gemspec +7 -2
- metadata +75 -11
- data/.travis.yml +0 -26
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniSql
|
4
|
+
module Abstract
|
5
|
+
class PreparedCache
|
6
|
+
|
7
|
+
DEFAULT_MAX_SIZE = 500
|
8
|
+
|
9
|
+
def initialize(connection, max_size = nil)
|
10
|
+
@connection = connection
|
11
|
+
@max_size = max_size || DEFAULT_MAX_SIZE
|
12
|
+
@cache = {}
|
13
|
+
@counter = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare_statement(sql)
|
17
|
+
stm_key = "#{@connection.object_id}-#{sql}"
|
18
|
+
statement = @cache.delete(stm_key)
|
19
|
+
if statement
|
20
|
+
@cache[stm_key] = statement
|
21
|
+
else
|
22
|
+
statement = @cache[stm_key] = alloc(sql)
|
23
|
+
dealloc(@cache.shift.last) if @cache.length > @max_size
|
24
|
+
end
|
25
|
+
|
26
|
+
statement
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def next_key
|
32
|
+
"s#{@counter += 1}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def alloc(_)
|
36
|
+
raise NotImplementedError, "must be implemented by specific database driver"
|
37
|
+
end
|
38
|
+
|
39
|
+
def dealloc(_)
|
40
|
+
raise NotImplementedError, "must be implemented by specific database driver"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/mini_sql/builder.rb
CHANGED
@@ -3,27 +3,71 @@
|
|
3
3
|
class MiniSql::Builder
|
4
4
|
|
5
5
|
def initialize(connection, template)
|
6
|
-
@args =
|
6
|
+
@args = {}
|
7
7
|
@sql = template
|
8
8
|
@sections = {}
|
9
9
|
@connection = connection
|
10
|
+
@count_variables = 1
|
11
|
+
@is_prepared = false
|
10
12
|
end
|
11
13
|
|
12
|
-
[:set, :where2, :where, :order_by, :
|
13
|
-
define_method k do |
|
14
|
-
if
|
15
|
-
@args ||= {}
|
14
|
+
[:set, :where2, :where, :order_by, :left_join, :join, :select, :group_by].each do |k|
|
15
|
+
define_method k do |sql_part, *args|
|
16
|
+
if Hash === args[0]
|
16
17
|
@args.merge!(args[0])
|
17
|
-
|
18
|
-
|
18
|
+
else # convert simple params to hash
|
19
|
+
args.each do |v|
|
20
|
+
param = "_m_#{@count_variables += 1}"
|
21
|
+
sql_part = sql_part.sub('?', ":#{param}")
|
22
|
+
@args[param] = v
|
23
|
+
end
|
19
24
|
end
|
25
|
+
|
20
26
|
@sections[k] ||= []
|
21
|
-
@sections[k] <<
|
27
|
+
@sections[k] << sql_part
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
[:limit, :offset].each do |k|
|
33
|
+
define_method k do |value|
|
34
|
+
@args["_m_#{k}"] = value
|
35
|
+
@sections[k] = true
|
22
36
|
self
|
23
37
|
end
|
24
38
|
end
|
25
39
|
|
26
|
-
|
40
|
+
[:query, :query_single, :query_hash, :query_array, :exec].each do |m|
|
41
|
+
class_eval <<~RUBY
|
42
|
+
def #{m}(hash_args = nil)
|
43
|
+
connection_switcher.#{m}(parametrized_sql, union_parameters(hash_args))
|
44
|
+
end
|
45
|
+
RUBY
|
46
|
+
end
|
47
|
+
|
48
|
+
def query_decorator(decorator, hash_args = nil)
|
49
|
+
connection_switcher.query_decorator(decorator, parametrized_sql, union_parameters(hash_args))
|
50
|
+
end
|
51
|
+
|
52
|
+
def prepared(condition = true)
|
53
|
+
@is_prepared = condition
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_sql(hash_args = nil)
|
59
|
+
@connection.param_encoder.encode(parametrized_sql, union_parameters(hash_args))
|
60
|
+
end
|
61
|
+
|
62
|
+
private def connection_switcher
|
63
|
+
if @is_prepared
|
64
|
+
@connection.prepared
|
65
|
+
else
|
66
|
+
@connection
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private def parametrized_sql
|
27
71
|
sql = @sql.dup
|
28
72
|
|
29
73
|
@sections.each do |k, v|
|
@@ -38,33 +82,29 @@ class MiniSql::Builder
|
|
38
82
|
when :left_join
|
39
83
|
joined = v.map { |item| (+"LEFT JOIN ") << item }.join("\n")
|
40
84
|
when :limit
|
41
|
-
joined = (+"LIMIT ")
|
85
|
+
joined = (+"LIMIT :_m_limit")
|
42
86
|
when :offset
|
43
|
-
joined = (+"OFFSET ")
|
87
|
+
joined = (+"OFFSET :_m_offset")
|
44
88
|
when :order_by
|
45
89
|
joined = (+"ORDER BY ") << v.join(" , ")
|
90
|
+
when :group_by
|
91
|
+
joined = (+"GROUP BY ") << v.join(" , ")
|
46
92
|
when :set
|
47
93
|
joined = (+"SET ") << v.join(" , ")
|
48
94
|
end
|
49
95
|
|
50
96
|
sql.sub!("/*#{k}*/", joined)
|
51
97
|
end
|
98
|
+
|
52
99
|
sql
|
53
100
|
end
|
54
101
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
@connection.#{m}(to_sql, hash_args)
|
62
|
-
else
|
63
|
-
@connection.#{m}(to_sql)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
RUBY
|
102
|
+
private def union_parameters(hash_args)
|
103
|
+
if hash_args
|
104
|
+
@args.merge(hash_args)
|
105
|
+
else
|
106
|
+
@args
|
107
|
+
end
|
67
108
|
end
|
68
109
|
|
69
110
|
end
|
70
|
-
|
data/lib/mini_sql/connection.rb
CHANGED
@@ -4,7 +4,7 @@ module MiniSql
|
|
4
4
|
class Connection
|
5
5
|
|
6
6
|
def self.get(raw_connection, options = {})
|
7
|
-
if (defined? ::PG::Connection) && (PG::Connection === raw_connection)
|
7
|
+
if (defined? ::PG::Connection) && (PG::Connection === raw_connection)
|
8
8
|
Postgres::Connection.new(raw_connection, options)
|
9
9
|
elsif (defined? ::ArJdbc)
|
10
10
|
Postgres::Connection.new(raw_connection, options)
|
@@ -31,11 +31,23 @@ module MiniSql
|
|
31
31
|
raise NotImplementedError, "must be implemented by child connection"
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
34
|
+
def query_hash(sql, *params)
|
35
35
|
raise NotImplementedError, "must be implemented by child connection"
|
36
36
|
end
|
37
37
|
|
38
|
-
def
|
38
|
+
def query_decorator(sql, *params)
|
39
|
+
raise NotImplementedError, "must be implemented by child connection"
|
40
|
+
end
|
41
|
+
|
42
|
+
def query_each(sql, *params)
|
43
|
+
raise NotImplementedError, "must be implemented by child connection"
|
44
|
+
end
|
45
|
+
|
46
|
+
def query_each_hash(sql, *params)
|
47
|
+
raise NotImplementedError, "must be implemented by child connection"
|
48
|
+
end
|
49
|
+
|
50
|
+
def exec(sql, *params)
|
39
51
|
raise NotImplementedError, "must be implemented by child connection"
|
40
52
|
end
|
41
53
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniSql
|
4
|
+
module Decoratable
|
5
|
+
def decorated(mod)
|
6
|
+
@decoratorated_classes ||= {}
|
7
|
+
@decoratorated_classes[mod] ||=
|
8
|
+
Class.new(self) do
|
9
|
+
include(mod)
|
10
|
+
instance_eval <<~RUBY
|
11
|
+
def decorator
|
12
|
+
#{mod}
|
13
|
+
end
|
14
|
+
RUBY
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def decorator
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -9,8 +9,6 @@ module MiniSql
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def encode(sql, *params)
|
12
|
-
return sql unless params && params.length > 0
|
13
|
-
|
14
12
|
if Hash === (hash = params[0])
|
15
13
|
raise ArgumentError, "Only one hash param is allowed, multiple were sent" if params.length > 1
|
16
14
|
encode_hash(sql, hash)
|
@@ -38,31 +36,32 @@ module MiniSql
|
|
38
36
|
|
39
37
|
def encode_array(sql, array)
|
40
38
|
i = -1
|
41
|
-
sql.gsub("?") do
|
39
|
+
sql.gsub("?") do
|
42
40
|
i += 1
|
43
41
|
quote_val(array[i])
|
44
42
|
end
|
45
43
|
end
|
46
44
|
|
47
|
-
def
|
45
|
+
def quoted_time(value)
|
48
46
|
value.utc.iso8601
|
49
47
|
end
|
50
48
|
|
51
49
|
def quote_val(value)
|
52
50
|
case value
|
51
|
+
when String then "'#{conn.escape_string(value.to_s)}'"
|
52
|
+
when Numeric then value.to_s
|
53
|
+
when BigDecimal then value.to_s("F")
|
54
|
+
when Time then "'#{quoted_time(value)}'"
|
55
|
+
when Date then "'#{value.to_s}'"
|
56
|
+
when Symbol then "'#{conn.escape_string(value.to_s)}'"
|
57
|
+
when true then "true"
|
58
|
+
when false then "false"
|
59
|
+
when nil then "NULL"
|
60
|
+
when [] then "NULL"
|
53
61
|
when Array
|
54
62
|
value.map do |v|
|
55
63
|
quote_val(v)
|
56
64
|
end.join(', ')
|
57
|
-
when String
|
58
|
-
"'#{conn.escape_string(value.to_s)}'"
|
59
|
-
when true then "true"
|
60
|
-
when false then "false"
|
61
|
-
when nil then "NULL"
|
62
|
-
when BigDecimal then value.to_s("F")
|
63
|
-
when Numeric then value.to_s
|
64
|
-
when Date, Time then "'#{quoted_date(value)}'"
|
65
|
-
when Symbol then "'#{conn.escape_string(value.to_s)}'"
|
66
65
|
else raise TypeError, "can't quote #{value.class.name}"
|
67
66
|
end
|
68
67
|
end
|
@@ -9,6 +9,12 @@ module MiniSql
|
|
9
9
|
@raw_connection = raw_connection
|
10
10
|
@param_encoder = (args && args[:param_encoder]) || InlineParamEncoder.new(self)
|
11
11
|
@deserializer_cache = (args && args[:deserializer_cache]) || DeserializerCache.new
|
12
|
+
|
13
|
+
@prepared = PreparedConnection.new(self, @deserializer_cache)
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepared(condition = true)
|
17
|
+
condition ? @prepared : self
|
12
18
|
end
|
13
19
|
|
14
20
|
def query_single(sql, *params)
|
@@ -20,6 +26,10 @@ module MiniSql
|
|
20
26
|
result.to_a
|
21
27
|
end
|
22
28
|
|
29
|
+
def query_array(sql, *params)
|
30
|
+
run(sql, :array, params).to_a
|
31
|
+
end
|
32
|
+
|
23
33
|
def exec(sql, *params)
|
24
34
|
run(sql, :array, params)
|
25
35
|
raw_connection.affected_rows
|
@@ -30,6 +40,11 @@ module MiniSql
|
|
30
40
|
@deserializer_cache.materialize(result)
|
31
41
|
end
|
32
42
|
|
43
|
+
def query_decorator(decorator, sql, *params)
|
44
|
+
result = run(sql, :array, params)
|
45
|
+
@deserializer_cache.materialize(result, decorator)
|
46
|
+
end
|
47
|
+
|
33
48
|
def escape_string(str)
|
34
49
|
raw_connection.escape(str)
|
35
50
|
end
|
@@ -45,9 +60,9 @@ module MiniSql
|
|
45
60
|
sql = param_encoder.encode(sql, *params)
|
46
61
|
end
|
47
62
|
raw_connection.query(
|
48
|
-
sql,
|
49
|
-
as: as,
|
50
|
-
database_timezone: :utc,
|
63
|
+
sql,
|
64
|
+
as: as,
|
65
|
+
database_timezone: :utc,
|
51
66
|
application_timezone: :utc,
|
52
67
|
cast_booleans: true,
|
53
68
|
cast: true,
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MiniSql
|
2
4
|
module Mysql
|
3
5
|
class DeserializerCache
|
@@ -9,18 +11,22 @@ module MiniSql
|
|
9
11
|
@max_size = max_size || DEFAULT_MAX_SIZE
|
10
12
|
end
|
11
13
|
|
12
|
-
def materialize(result)
|
13
|
-
key = result.fields
|
14
|
+
def materialize(result, decorator_module = nil)
|
15
|
+
key = result.fields.join(',')
|
14
16
|
|
15
17
|
# trivial fast LRU implementation
|
16
18
|
materializer = @cache.delete(key)
|
17
19
|
if materializer
|
18
20
|
@cache[key] = materializer
|
19
21
|
else
|
20
|
-
materializer = @cache[key] =
|
22
|
+
materializer = @cache[key] = new_row_materializer(result)
|
21
23
|
@cache.shift if @cache.length > @max_size
|
22
24
|
end
|
23
25
|
|
26
|
+
if decorator_module
|
27
|
+
materializer = materializer.decorated(decorator_module)
|
28
|
+
end
|
29
|
+
|
24
30
|
result.map do |data|
|
25
31
|
materializer.materialize(data)
|
26
32
|
end
|
@@ -28,27 +34,19 @@ module MiniSql
|
|
28
34
|
|
29
35
|
private
|
30
36
|
|
31
|
-
def
|
37
|
+
def new_row_materializer(result)
|
32
38
|
fields = result.fields
|
33
39
|
|
34
40
|
Class.new do
|
35
|
-
|
41
|
+
extend MiniSql::Decoratable
|
42
|
+
include MiniSql::Result
|
36
43
|
|
37
|
-
|
38
|
-
alias :read_attribute_for_serialization :send
|
39
|
-
|
40
|
-
def to_h
|
41
|
-
r = {}
|
42
|
-
instance_variables.each do |f|
|
43
|
-
r[f.to_s.sub('@','').to_sym] = instance_variable_get(f)
|
44
|
-
end
|
45
|
-
r
|
46
|
-
end
|
44
|
+
attr_accessor(*fields)
|
47
45
|
|
48
46
|
instance_eval <<~RUBY
|
49
47
|
def materialize(data)
|
50
48
|
r = self.new
|
51
|
-
#{col
|
49
|
+
#{col = -1; fields.map { |f| "r.#{f} = data[#{col += 1}]" }.join("; ")}
|
52
50
|
r
|
53
51
|
end
|
54
52
|
RUBY
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mini_sql/abstract/prepared_cache"
|
4
|
+
|
5
|
+
module MiniSql
|
6
|
+
module Mysql
|
7
|
+
class PreparedCache < ::MiniSql::Abstract::PreparedCache
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def alloc(sql)
|
12
|
+
@connection.prepare(sql)
|
13
|
+
end
|
14
|
+
|
15
|
+
def dealloc(statement)
|
16
|
+
statement.close unless statement.closed?
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniSql
|
4
|
+
module Mysql
|
5
|
+
class PreparedConnection < Connection
|
6
|
+
|
7
|
+
attr_reader :unprepared
|
8
|
+
|
9
|
+
def initialize(unprepared_connection, deserializer_cache)
|
10
|
+
@unprepared = unprepared_connection
|
11
|
+
@raw_connection = unprepared_connection.raw_connection
|
12
|
+
@deserializer_cache = deserializer_cache
|
13
|
+
@param_encoder = unprepared_connection.param_encoder
|
14
|
+
|
15
|
+
@prepared_cache = PreparedCache.new(@raw_connection)
|
16
|
+
@param_binder = PreparedBinds.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def build(_)
|
20
|
+
raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepared(condition = true)
|
24
|
+
condition ? self : @unprepared
|
25
|
+
end
|
26
|
+
|
27
|
+
private def run(sql, as, params)
|
28
|
+
prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
|
29
|
+
statement = @prepared_cache.prepare_statement(prepared_sql)
|
30
|
+
statement.execute(
|
31
|
+
*binds,
|
32
|
+
as: as,
|
33
|
+
database_timezone: :utc,
|
34
|
+
application_timezone: :utc,
|
35
|
+
cast_booleans: true,
|
36
|
+
cast: true,
|
37
|
+
cache_rows: true,
|
38
|
+
symbolize_keys: false
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|