mini_sql 1.0 → 1.1.3

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/CHANGELOG +20 -0
  4. data/README.md +36 -0
  5. data/bench/builder_perf.rb +138 -0
  6. data/bench/decorator_perf.rb +143 -0
  7. data/bench/mini_sql_methods_perf.rb +80 -0
  8. data/bench/prepared_perf.rb +59 -0
  9. data/bench/shared/generate_data.rb +133 -0
  10. data/bench/topic_perf.rb +21 -327
  11. data/bench/topic_wide_perf.rb +92 -0
  12. data/lib/mini_sql.rb +17 -8
  13. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  14. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  15. data/lib/mini_sql/builder.rb +63 -30
  16. data/lib/mini_sql/inline_param_encoder.rb +4 -5
  17. data/lib/mini_sql/mysql/connection.rb +13 -3
  18. data/lib/mini_sql/mysql/deserializer_cache.rb +3 -3
  19. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  20. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  21. data/lib/mini_sql/mysql/prepared_connection.rb +47 -0
  22. data/lib/mini_sql/postgres/connection.rb +21 -7
  23. data/lib/mini_sql/postgres/deserializer_cache.rb +5 -5
  24. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  25. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  26. data/lib/mini_sql/postgres/prepared_connection.rb +39 -0
  27. data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
  28. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +3 -3
  29. data/lib/mini_sql/result.rb +10 -0
  30. data/lib/mini_sql/serializer.rb +29 -15
  31. data/lib/mini_sql/sqlite/connection.rb +14 -2
  32. data/lib/mini_sql/sqlite/deserializer_cache.rb +3 -3
  33. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  34. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  35. data/lib/mini_sql/sqlite/prepared_connection.rb +43 -0
  36. data/lib/mini_sql/version.rb +1 -1
  37. 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, "mini_sql/postgres_jdbc/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, "mini_sql/postgres/coders"
24
- autoload :Connection, "mini_sql/postgres/connection"
25
- autoload :DeserializerCache, "mini_sql/postgres/deserializer_cache"
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, "mini_sql/sqlite/connection"
30
- autoload :DeserializerCache, "mini_sql/sqlite/deserializer_cache"
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, "mini_sql/mysql/connection"
35
- autoload :DeserializerCache, "mini_sql/mysql/deserializer_cache"
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
@@ -3,27 +3,73 @@
3
3
  class MiniSql::Builder
4
4
 
5
5
  def initialize(connection, template)
6
- @args = nil
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, :limit, :left_join, :join, :offset, :select].each do |k|
13
- define_method k do |data, *args|
14
- if args && (args.length == 1) && (Hash === args[0])
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
- elsif args && args.length > 0
18
- data = @connection.param_encoder.encode(data, *args)
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] << data
29
+ @sections[k] << sql_part
22
30
  self
23
31
  end
24
32
  end
25
33
 
26
- def to_sql
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 ") << v.last.to_i.to_s
87
+ joined = (+"LIMIT :mq_auto_limit")
42
88
  when :offset
43
- joined = (+"OFFSET ") << v.last.to_i.to_s
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
- [:query, :query_single, :query_hash, :exec].each do |m|
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 query_decorator(decorator, hash_args = nil)
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
- @connection.query_decorator(decorator, to_sql, hash_args)
106
+ @args.merge(hash_args)
74
107
  else
75
- @connection.query_decorator(decorator, to_sql)
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 |p|
39
+ sql.gsub("?") do
42
40
  i += 1
43
41
  quote_val(array[i])
44
42
  end
45
43
  end
46
44
 
47
- def quoted_date(value)
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 Date, Time then "'#{quoted_date(value)}'"
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
- @deserializer_cache.materialize(result)
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
- @deserializer_cache.materialize(result, decorator)
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
- sql = param_encoder.encode(sql, *params)
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,