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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/CHANGELOG +4 -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 +61 -30
  16. data/lib/mini_sql/inline_param_encoder.rb +4 -3
  17. data/lib/mini_sql/mysql/connection.rb +6 -0
  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 +44 -0
  22. data/lib/mini_sql/postgres/connection.rb +6 -0
  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 +36 -0
  27. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +3 -3
  28. data/lib/mini_sql/result.rb +10 -0
  29. data/lib/mini_sql/serializer.rb +29 -15
  30. data/lib/mini_sql/sqlite/connection.rb +9 -1
  31. data/lib/mini_sql/sqlite/deserializer_cache.rb +3 -3
  32. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  33. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  34. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  35. data/lib/mini_sql/version.rb +1 -1
  36. 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, "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,71 @@
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
+ 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] << data
27
+ @sections[k] << sql_part
22
28
  self
23
29
  end
24
30
  end
25
31
 
26
- def to_sql
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 ") << v.last.to_i.to_s
85
+ joined = (+"LIMIT :_m_limit")
42
86
  when :offset
43
- joined = (+"OFFSET ") << v.last.to_i.to_s
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
- [: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
99
+ sql
67
100
  end
68
101
 
69
- def query_decorator(decorator, hash_args = nil)
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
- @connection.query_decorator(decorator, to_sql, hash_args)
104
+ @args.merge(hash_args)
74
105
  else
75
- @connection.query_decorator(decorator, to_sql)
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 |p|
39
+ sql.gsub("?") do
40
40
  i += 1
41
41
  quote_val(array[i])
42
42
  end
43
43
  end
44
44
 
45
- def quoted_date(value)
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 Date, Time then "'#{quoted_date(value)}'"
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] = new_row_matrializer(result)
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 new_row_matrializer(result)
37
+ def new_row_materializer(result)
38
38
  fields = result.fields
39
39
 
40
40
  Class.new do