mini_sql 1.0.1 → 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 +1 -1
- data/CHANGELOG +4 -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 +61 -30
- data/lib/mini_sql/inline_param_encoder.rb +4 -3
- data/lib/mini_sql/mysql/connection.rb +6 -0
- 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 +44 -0
- data/lib/mini_sql/postgres/connection.rb +6 -0
- 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 +36 -0
- 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 +9 -1
- 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 +40 -0
- data/lib/mini_sql/version.rb +1 -1
- metadata +19 -2
@@ -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,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
|
22
28
|
self
|
23
29
|
end
|
24
30
|
end
|
25
31
|
|
26
|
-
|
32
|
+
[:limit, :offset].each do |k|
|
33
|
+
define_method k do |value|
|
34
|
+
@args["_m_#{k}"] = value
|
35
|
+
@sections[k] = true
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
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,41 +82,28 @@ 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
|
52
|
-
sql
|
53
|
-
end
|
54
98
|
|
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
|
99
|
+
sql
|
67
100
|
end
|
68
101
|
|
69
|
-
def
|
70
|
-
hash_args = @args.merge(hash_args) if hash_args && @args
|
71
|
-
hash_args ||= @args
|
102
|
+
private def union_parameters(hash_args)
|
72
103
|
if hash_args
|
73
|
-
@
|
104
|
+
@args.merge(hash_args)
|
74
105
|
else
|
75
|
-
@
|
106
|
+
@args
|
76
107
|
end
|
77
108
|
end
|
78
109
|
|
@@ -36,13 +36,13 @@ module MiniSql
|
|
36
36
|
|
37
37
|
def encode_array(sql, array)
|
38
38
|
i = -1
|
39
|
-
sql.gsub("?") do
|
39
|
+
sql.gsub("?") do
|
40
40
|
i += 1
|
41
41
|
quote_val(array[i])
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
def
|
45
|
+
def quoted_time(value)
|
46
46
|
value.utc.iso8601
|
47
47
|
end
|
48
48
|
|
@@ -51,7 +51,8 @@ module MiniSql
|
|
51
51
|
when String then "'#{conn.escape_string(value.to_s)}'"
|
52
52
|
when Numeric then value.to_s
|
53
53
|
when BigDecimal then value.to_s("F")
|
54
|
-
when
|
54
|
+
when Time then "'#{quoted_time(value)}'"
|
55
|
+
when Date then "'#{value.to_s}'"
|
55
56
|
when Symbol then "'#{conn.escape_string(value.to_s)}'"
|
56
57
|
when true then "true"
|
57
58
|
when false then "false"
|
@@ -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)
|
@@ -12,14 +12,14 @@ module MiniSql
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def materialize(result, decorator_module = nil)
|
15
|
-
key = result.fields
|
15
|
+
key = result.fields.join(',')
|
16
16
|
|
17
17
|
# trivial fast LRU implementation
|
18
18
|
materializer = @cache.delete(key)
|
19
19
|
if materializer
|
20
20
|
@cache[key] = materializer
|
21
21
|
else
|
22
|
-
materializer = @cache[key] =
|
22
|
+
materializer = @cache[key] = new_row_materializer(result)
|
23
23
|
@cache.shift if @cache.length > @max_size
|
24
24
|
end
|
25
25
|
|
@@ -34,7 +34,7 @@ module MiniSql
|
|
34
34
|
|
35
35
|
private
|
36
36
|
|
37
|
-
def
|
37
|
+
def new_row_materializer(result)
|
38
38
|
fields = result.fields
|
39
39
|
|
40
40
|
Class.new do
|