mini_sql 0.2.5 → 1.1.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +66 -0
  3. data/.rubocop.yml +5 -2
  4. data/CHANGELOG +22 -0
  5. data/README.md +66 -1
  6. data/bench/builder_perf.rb +138 -0
  7. data/bench/decorator_perf.rb +143 -0
  8. data/bench/mini_sql_methods_perf.rb +80 -0
  9. data/bench/prepared_perf.rb +59 -0
  10. data/bench/shared/generate_data.rb +133 -0
  11. data/bench/topic_perf.rb +21 -327
  12. data/bench/topic_wide_perf.rb +92 -0
  13. data/lib/mini_sql.rb +20 -8
  14. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  15. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  16. data/lib/mini_sql/builder.rb +66 -23
  17. data/lib/mini_sql/connection.rb +14 -2
  18. data/lib/mini_sql/decoratable.rb +22 -0
  19. data/lib/mini_sql/inline_param_encoder.rb +4 -5
  20. data/lib/mini_sql/mysql/connection.rb +6 -0
  21. data/lib/mini_sql/mysql/deserializer_cache.rb +9 -15
  22. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  23. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  24. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  25. data/lib/mini_sql/postgres/connection.rb +75 -3
  26. data/lib/mini_sql/postgres/deserializer_cache.rb +32 -15
  27. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  28. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  29. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  30. data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
  31. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +10 -14
  32. data/lib/mini_sql/result.rb +30 -0
  33. data/lib/mini_sql/serializer.rb +84 -0
  34. data/lib/mini_sql/sqlite/connection.rb +9 -1
  35. data/lib/mini_sql/sqlite/deserializer_cache.rb +9 -15
  36. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  37. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  38. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  39. data/lib/mini_sql/version.rb +1 -1
  40. data/mini_sql.gemspec +6 -5
  41. metadata +49 -15
  42. 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, "mini_sql/postgres_jdbc/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, "mini_sql/postgres/coders"
21
- autoload :Connection, "mini_sql/postgres/connection"
22
- 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"
23
29
  end
24
30
 
25
31
  module Sqlite
26
- autoload :Connection, "mini_sql/sqlite/connection"
27
- 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"
28
37
  end
29
38
 
30
39
  module Mysql
31
- autoload :Connection, "mini_sql/mysql/connection"
32
- 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"
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
@@ -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
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
- def to_sql
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 ") << 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
100
+
52
101
  sql
53
102
  end
54
103
 
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
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
@@ -31,11 +31,23 @@ module MiniSql
31
31
  raise NotImplementedError, "must be implemented by child connection"
32
32
  end
33
33
 
34
- def exec(sql, *params)
34
+ def query_hash(sql, *params)
35
35
  raise NotImplementedError, "must be implemented by child connection"
36
36
  end
37
37
 
38
- def query_hash(sql, *params)
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 |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"