mini_sql 1.0 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
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,