mini_sql 0.2.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|