mini_sql 1.0 → 1.1.3
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 +1 -1
- data/CHANGELOG +20 -0
- data/README.md +36 -0
- 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/topic_perf.rb +21 -327
- data/bench/topic_wide_perf.rb +92 -0
- data/lib/mini_sql.rb +17 -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 +63 -30
- data/lib/mini_sql/inline_param_encoder.rb +4 -5
- data/lib/mini_sql/mysql/connection.rb +13 -3
- data/lib/mini_sql/mysql/deserializer_cache.rb +3 -3
- 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 +47 -0
- data/lib/mini_sql/postgres/connection.rb +21 -7
- data/lib/mini_sql/postgres/deserializer_cache.rb +5 -5
- 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 +39 -0
- data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
- data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +3 -3
- data/lib/mini_sql/result.rb +10 -0
- data/lib/mini_sql/serializer.rb +29 -15
- data/lib/mini_sql/sqlite/connection.rb +14 -2
- data/lib/mini_sql/sqlite/deserializer_cache.rb +3 -3
- 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 +43 -0
- data/lib/mini_sql/version.rb +1 -1
- metadata +20 -3
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'pg', github: 'ged/ruby-pg'
|
8
|
+
gem 'mini_sql', path: '../'
|
9
|
+
gem 'activesupport'
|
10
|
+
gem 'activerecord'
|
11
|
+
gem 'activemodel'
|
12
|
+
gem 'memory_profiler'
|
13
|
+
gem 'benchmark-ips'
|
14
|
+
gem 'sequel', github: 'jeremyevans/sequel'
|
15
|
+
gem 'sequel_pg', github: 'jeremyevans/sequel_pg', require: 'sequel'
|
16
|
+
gem 'draper'
|
17
|
+
gem 'pry'
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'sequel'
|
21
|
+
require 'active_record'
|
22
|
+
require 'memory_profiler'
|
23
|
+
require 'benchmark/ips'
|
24
|
+
require 'mini_sql'
|
25
|
+
|
26
|
+
require '../mini_sql/bench/shared/generate_data'
|
27
|
+
|
28
|
+
ar_connection, conn_config = GenerateData.new(count_records: 1_000).call
|
29
|
+
PG_CONN = ar_connection.raw_connection
|
30
|
+
MINI_SQL = MiniSql::Connection.get(PG_CONN)
|
31
|
+
DB = Sequel.connect(ar_connection.instance_variable_get(:@config).slice(:database, :user, :password, :host, :adapter))
|
32
|
+
|
33
|
+
class Topic < ActiveRecord::Base
|
34
|
+
end
|
35
|
+
|
36
|
+
class TopicSequel < Sequel::Model(:topics)
|
37
|
+
end
|
38
|
+
|
39
|
+
def wide_topic_ar
|
40
|
+
Topic.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def wide_topic_pg
|
44
|
+
r = PG_CONN.async_exec("select * from topics limit 1")
|
45
|
+
row = r.first
|
46
|
+
r.clear
|
47
|
+
row
|
48
|
+
end
|
49
|
+
|
50
|
+
def wide_topic_sequel
|
51
|
+
TopicSequel.first
|
52
|
+
end
|
53
|
+
|
54
|
+
def wide_topic_mini_sql
|
55
|
+
PG_CONN.query("select * from topics limit 1").first
|
56
|
+
end
|
57
|
+
|
58
|
+
Benchmark.ips do |r|
|
59
|
+
r.report("wide topic ar") do |n|
|
60
|
+
while n > 0
|
61
|
+
wide_topic_ar
|
62
|
+
n -= 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
r.report("wide topic sequel") do |n|
|
66
|
+
while n > 0
|
67
|
+
wide_topic_sequel
|
68
|
+
n -= 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
r.report("wide topic pg") do |n|
|
72
|
+
while n > 0
|
73
|
+
wide_topic_pg
|
74
|
+
n -= 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
r.report("wide topic mini sql") do |n|
|
78
|
+
while n > 0
|
79
|
+
wide_topic_mini_sql
|
80
|
+
n -= 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
r.compare!
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
#
|
88
|
+
# Comparison:
|
89
|
+
# wide topic pg: 6974.6 i/s
|
90
|
+
# wide topic mini sql: 6760.9 i/s - same-ish: difference falls within error
|
91
|
+
# wide topic sequel: 5050.5 i/s - 1.38x (± 0.00) slower
|
92
|
+
# wide topic ar: 1565.4 i/s - 4.46x (± 0.00) slower
|
data/lib/mini_sql.rb
CHANGED
@@ -15,24 +15,33 @@ require_relative "mini_sql/result"
|
|
15
15
|
module MiniSql
|
16
16
|
if RUBY_ENGINE == 'jruby'
|
17
17
|
module Postgres
|
18
|
-
autoload :Connection,
|
18
|
+
autoload :Connection, "mini_sql/postgres_jdbc/connection"
|
19
19
|
autoload :DeserializerCache, "mini_sql/postgres_jdbc/deserializer_cache"
|
20
20
|
end
|
21
21
|
else
|
22
22
|
module Postgres
|
23
|
-
autoload :Coders,
|
24
|
-
autoload :Connection,
|
25
|
-
autoload :DeserializerCache,
|
23
|
+
autoload :Coders, "mini_sql/postgres/coders"
|
24
|
+
autoload :Connection, "mini_sql/postgres/connection"
|
25
|
+
autoload :DeserializerCache, "mini_sql/postgres/deserializer_cache"
|
26
|
+
autoload :PreparedConnection, "mini_sql/postgres/prepared_connection"
|
27
|
+
autoload :PreparedCache, "mini_sql/postgres/prepared_cache"
|
28
|
+
autoload :PreparedBinds, "mini_sql/postgres/prepared_binds"
|
26
29
|
end
|
27
30
|
|
28
31
|
module Sqlite
|
29
|
-
autoload :Connection,
|
30
|
-
autoload :DeserializerCache,
|
32
|
+
autoload :Connection, "mini_sql/sqlite/connection"
|
33
|
+
autoload :DeserializerCache, "mini_sql/sqlite/deserializer_cache"
|
34
|
+
autoload :PreparedCache, "mini_sql/sqlite/prepared_cache"
|
35
|
+
autoload :PreparedBinds, "mini_sql/sqlite/prepared_binds"
|
36
|
+
autoload :PreparedConnection, "mini_sql/sqlite/prepared_connection"
|
31
37
|
end
|
32
38
|
|
33
39
|
module Mysql
|
34
|
-
autoload :Connection,
|
35
|
-
autoload :DeserializerCache,
|
40
|
+
autoload :Connection, "mini_sql/mysql/connection"
|
41
|
+
autoload :DeserializerCache, "mini_sql/mysql/deserializer_cache"
|
42
|
+
autoload :PreparedCache, "mini_sql/mysql/prepared_cache"
|
43
|
+
autoload :PreparedBinds, "mini_sql/mysql/prepared_binds"
|
44
|
+
autoload :PreparedConnection, "mini_sql/mysql/prepared_connection"
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniSql
|
4
|
+
module Abstract
|
5
|
+
class PreparedBinds
|
6
|
+
|
7
|
+
# For compatibility with Active Record
|
8
|
+
BindName = Struct.new(:name)
|
9
|
+
|
10
|
+
def bind(sql, *params)
|
11
|
+
if Hash === (hash = params[0])
|
12
|
+
bind_hash(sql, hash)
|
13
|
+
else
|
14
|
+
bind_array(sql, params)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def bind_hash(sql, hash)
|
19
|
+
sql = sql.dup
|
20
|
+
binds = []
|
21
|
+
bind_names = []
|
22
|
+
i = 0
|
23
|
+
|
24
|
+
hash.each do |k, v|
|
25
|
+
sql.gsub!(":#{k}") do
|
26
|
+
# ignore ::int and stuff like that
|
27
|
+
# $` is previous to match
|
28
|
+
if $` && $`[-1] != ":"
|
29
|
+
array_wrap(v).map do |vv|
|
30
|
+
binds << vv
|
31
|
+
bind_names << [BindName.new(k)]
|
32
|
+
bind_output(i += 1)
|
33
|
+
end.join(', ')
|
34
|
+
else
|
35
|
+
":#{k}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
[sql, binds, bind_names]
|
40
|
+
end
|
41
|
+
|
42
|
+
def bind_array(sql, array)
|
43
|
+
sql = sql.dup
|
44
|
+
param_i = 0
|
45
|
+
i = 0
|
46
|
+
binds = []
|
47
|
+
bind_names = []
|
48
|
+
sql.gsub!("?") do
|
49
|
+
param_i += 1
|
50
|
+
array_wrap(array[param_i - 1]).map do |vv|
|
51
|
+
binds << vv
|
52
|
+
i += 1
|
53
|
+
bind_names << [BindName.new("$#{i}")]
|
54
|
+
bind_output(i)
|
55
|
+
end.join(', ')
|
56
|
+
end
|
57
|
+
[sql, binds, bind_names]
|
58
|
+
end
|
59
|
+
|
60
|
+
def array_wrap(object)
|
61
|
+
if object.respond_to?(:to_ary)
|
62
|
+
object.to_ary || [object]
|
63
|
+
else
|
64
|
+
[object]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def bind_output(_)
|
69
|
+
raise NotImplementedError, "must be implemented by specific database driver"
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -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,73 @@
|
|
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
|
+
# for compatability with AR param encoded we keep a non _
|
21
|
+
# prefix (must be [a-z])
|
22
|
+
param = "mq_auto_#{@count_variables += 1}"
|
23
|
+
sql_part = sql_part.sub('?', ":#{param}")
|
24
|
+
@args[param.to_sym] = v
|
25
|
+
end
|
19
26
|
end
|
27
|
+
|
20
28
|
@sections[k] ||= []
|
21
|
-
@sections[k] <<
|
29
|
+
@sections[k] << sql_part
|
22
30
|
self
|
23
31
|
end
|
24
32
|
end
|
25
33
|
|
26
|
-
|
34
|
+
[:limit, :offset].each do |k|
|
35
|
+
define_method k do |value|
|
36
|
+
@args["mq_auto_#{k}".to_sym] = value
|
37
|
+
@sections[k] = true
|
38
|
+
self
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
[:query, :query_single, :query_hash, :query_array, :exec].each do |m|
|
43
|
+
class_eval <<~RUBY
|
44
|
+
def #{m}(hash_args = nil)
|
45
|
+
connection_switcher.#{m}(parametrized_sql, union_parameters(hash_args))
|
46
|
+
end
|
47
|
+
RUBY
|
48
|
+
end
|
49
|
+
|
50
|
+
def query_decorator(decorator, hash_args = nil)
|
51
|
+
connection_switcher.query_decorator(decorator, parametrized_sql, union_parameters(hash_args))
|
52
|
+
end
|
53
|
+
|
54
|
+
def prepared(condition = true)
|
55
|
+
@is_prepared = condition
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_sql(hash_args = nil)
|
61
|
+
@connection.param_encoder.encode(parametrized_sql, union_parameters(hash_args))
|
62
|
+
end
|
63
|
+
|
64
|
+
private def connection_switcher
|
65
|
+
if @is_prepared
|
66
|
+
@connection.prepared
|
67
|
+
else
|
68
|
+
@connection
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private def parametrized_sql
|
27
73
|
sql = @sql.dup
|
28
74
|
|
29
75
|
@sections.each do |k, v|
|
@@ -38,41 +84,28 @@ class MiniSql::Builder
|
|
38
84
|
when :left_join
|
39
85
|
joined = v.map { |item| (+"LEFT JOIN ") << item }.join("\n")
|
40
86
|
when :limit
|
41
|
-
joined = (+"LIMIT ")
|
87
|
+
joined = (+"LIMIT :mq_auto_limit")
|
42
88
|
when :offset
|
43
|
-
joined = (+"OFFSET ")
|
89
|
+
joined = (+"OFFSET :mq_auto_offset")
|
44
90
|
when :order_by
|
45
91
|
joined = (+"ORDER BY ") << v.join(" , ")
|
92
|
+
when :group_by
|
93
|
+
joined = (+"GROUP BY ") << v.join(" , ")
|
46
94
|
when :set
|
47
95
|
joined = (+"SET ") << v.join(" , ")
|
48
96
|
end
|
49
97
|
|
50
98
|
sql.sub!("/*#{k}*/", joined)
|
51
99
|
end
|
52
|
-
sql
|
53
|
-
end
|
54
100
|
|
55
|
-
|
56
|
-
class_eval <<~RUBY
|
57
|
-
def #{m}(hash_args = nil)
|
58
|
-
hash_args = @args.merge(hash_args) if hash_args && @args
|
59
|
-
hash_args ||= @args
|
60
|
-
if hash_args
|
61
|
-
@connection.#{m}(to_sql, hash_args)
|
62
|
-
else
|
63
|
-
@connection.#{m}(to_sql)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
RUBY
|
101
|
+
sql
|
67
102
|
end
|
68
103
|
|
69
|
-
def
|
70
|
-
hash_args = @args.merge(hash_args) if hash_args && @args
|
71
|
-
hash_args ||= @args
|
104
|
+
private def union_parameters(hash_args)
|
72
105
|
if hash_args
|
73
|
-
@
|
106
|
+
@args.merge(hash_args)
|
74
107
|
else
|
75
|
-
@
|
108
|
+
@args
|
76
109
|
end
|
77
110
|
end
|
78
111
|
|
@@ -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,13 +36,13 @@ 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
|
|
@@ -53,7 +51,8 @@ module MiniSql
|
|
53
51
|
when String then "'#{conn.escape_string(value.to_s)}'"
|
54
52
|
when Numeric then value.to_s
|
55
53
|
when BigDecimal then value.to_s("F")
|
56
|
-
when
|
54
|
+
when Time then "'#{quoted_time(value)}'"
|
55
|
+
when Date then "'#{value.to_s}'"
|
57
56
|
when Symbol then "'#{conn.escape_string(value.to_s)}'"
|
58
57
|
when true then "true"
|
59
58
|
when false then "false"
|
@@ -11,6 +11,14 @@ module MiniSql
|
|
11
11
|
@deserializer_cache = (args && args[:deserializer_cache]) || DeserializerCache.new
|
12
12
|
end
|
13
13
|
|
14
|
+
def prepared(condition = true)
|
15
|
+
if condition
|
16
|
+
@prepared ||= PreparedConnection.new(self)
|
17
|
+
else
|
18
|
+
self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
14
22
|
def query_single(sql, *params)
|
15
23
|
run(sql, :array, params).to_a.flatten!
|
16
24
|
end
|
@@ -31,12 +39,12 @@ module MiniSql
|
|
31
39
|
|
32
40
|
def query(sql, *params)
|
33
41
|
result = run(sql, :array, params)
|
34
|
-
|
42
|
+
deserializer_cache.materialize(result)
|
35
43
|
end
|
36
44
|
|
37
45
|
def query_decorator(decorator, sql, *params)
|
38
46
|
result = run(sql, :array, params)
|
39
|
-
|
47
|
+
deserializer_cache.materialize(result, decorator)
|
40
48
|
end
|
41
49
|
|
42
50
|
def escape_string(str)
|
@@ -50,7 +58,9 @@ module MiniSql
|
|
50
58
|
private
|
51
59
|
|
52
60
|
def run(sql, as, params)
|
53
|
-
|
61
|
+
if params && params.length > 0
|
62
|
+
sql = param_encoder.encode(sql, *params)
|
63
|
+
end
|
54
64
|
raw_connection.query(
|
55
65
|
sql,
|
56
66
|
as: as,
|