mini_sql 0.2.5 → 1.1.1
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.yml +5 -2
- data/CHANGELOG +22 -0
- data/README.md +66 -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/topic_perf.rb +21 -327
- data/bench/topic_wide_perf.rb +92 -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 +66 -23
- data/lib/mini_sql/connection.rb +14 -2
- data/lib/mini_sql/decoratable.rb +22 -0
- data/lib/mini_sql/inline_param_encoder.rb +4 -5
- data/lib/mini_sql/mysql/connection.rb +6 -0
- data/lib/mini_sql/mysql/deserializer_cache.rb +9 -15
- 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 +75 -3
- data/lib/mini_sql/postgres/deserializer_cache.rb +32 -15
- 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 +3 -1
- data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +10 -14
- data/lib/mini_sql/result.rb +30 -0
- data/lib/mini_sql/serializer.rb +84 -0
- data/lib/mini_sql/sqlite/connection.rb +9 -1
- data/lib/mini_sql/sqlite/deserializer_cache.rb +9 -15
- 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 +6 -5
- metadata +49 -15
- data/.travis.yml +0 -28
@@ -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
@@ -8,28 +8,40 @@ require_relative "mini_sql/connection"
|
|
8
8
|
require_relative "mini_sql/deserializer_cache"
|
9
9
|
require_relative "mini_sql/builder"
|
10
10
|
require_relative "mini_sql/inline_param_encoder"
|
11
|
+
require_relative "mini_sql/decoratable"
|
12
|
+
require_relative "mini_sql/serializer"
|
13
|
+
require_relative "mini_sql/result"
|
11
14
|
|
12
15
|
module MiniSql
|
13
16
|
if RUBY_ENGINE == 'jruby'
|
14
17
|
module Postgres
|
15
|
-
autoload :Connection,
|
18
|
+
autoload :Connection, "mini_sql/postgres_jdbc/connection"
|
16
19
|
autoload :DeserializerCache, "mini_sql/postgres_jdbc/deserializer_cache"
|
17
20
|
end
|
18
21
|
else
|
19
22
|
module Postgres
|
20
|
-
autoload :Coders,
|
21
|
-
autoload :Connection,
|
22
|
-
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"
|
23
29
|
end
|
24
30
|
|
25
31
|
module Sqlite
|
26
|
-
autoload :Connection,
|
27
|
-
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"
|
28
37
|
end
|
29
38
|
|
30
39
|
module Mysql
|
31
|
-
autoload :Connection,
|
32
|
-
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"
|
33
45
|
end
|
34
46
|
end
|
35
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
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
[:limit, :offset].each do |k|
|
35
|
+
define_method k do |value|
|
36
|
+
@args["mq_auto_#{k}".to_sym] = value
|
37
|
+
@sections[k] = true
|
22
38
|
self
|
23
39
|
end
|
24
40
|
end
|
25
41
|
|
26
|
-
|
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,32 +84,29 @@ 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
|
100
|
+
|
52
101
|
sql
|
53
102
|
end
|
54
103
|
|
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
|
104
|
+
private def union_parameters(hash_args)
|
105
|
+
if hash_args
|
106
|
+
@args.merge(hash_args)
|
107
|
+
else
|
108
|
+
@args
|
109
|
+
end
|
67
110
|
end
|
68
111
|
|
69
112
|
end
|
data/lib/mini_sql/connection.rb
CHANGED
@@ -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,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"
|