mini_sql 0.2.4 → 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +66 -0
  3. data/.rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml +355 -0
  4. data/.rubocop.yml +8 -0
  5. data/CHANGELOG +22 -0
  6. data/Gemfile +3 -1
  7. data/Guardfile +2 -0
  8. data/README.md +125 -1
  9. data/Rakefile +3 -1
  10. data/bench/builder_perf.rb +138 -0
  11. data/bench/decorator_perf.rb +143 -0
  12. data/bench/mini_sql_methods_perf.rb +80 -0
  13. data/bench/prepared_perf.rb +59 -0
  14. data/bench/shared/generate_data.rb +133 -0
  15. data/bench/timestamp_perf.rb +22 -21
  16. data/bench/topic_mysql_perf.rb +1 -7
  17. data/bench/topic_perf.rb +27 -169
  18. data/bench/topic_wide_perf.rb +92 -0
  19. data/bin/console +1 -0
  20. data/lib/mini_sql.rb +20 -8
  21. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  22. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  23. data/lib/mini_sql/builder.rb +64 -24
  24. data/lib/mini_sql/connection.rb +15 -3
  25. data/lib/mini_sql/decoratable.rb +22 -0
  26. data/lib/mini_sql/deserializer_cache.rb +2 -0
  27. data/lib/mini_sql/inline_param_encoder.rb +12 -13
  28. data/lib/mini_sql/mysql/connection.rb +18 -3
  29. data/lib/mini_sql/mysql/deserializer_cache.rb +14 -16
  30. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  31. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  32. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  33. data/lib/mini_sql/postgres/coders.rb +2 -0
  34. data/lib/mini_sql/postgres/connection.rb +89 -0
  35. data/lib/mini_sql/postgres/deserializer_cache.rb +36 -16
  36. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  37. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  38. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  39. data/lib/mini_sql/postgres_jdbc/connection.rb +8 -1
  40. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +43 -43
  41. data/lib/mini_sql/result.rb +30 -0
  42. data/lib/mini_sql/serializer.rb +84 -0
  43. data/lib/mini_sql/sqlite/connection.rb +20 -2
  44. data/lib/mini_sql/sqlite/deserializer_cache.rb +14 -16
  45. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  46. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  47. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  48. data/lib/mini_sql/version.rb +1 -1
  49. data/mini_sql.gemspec +7 -2
  50. metadata +75 -11
  51. data/.travis.yml +0 -26
@@ -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
28
+ self
29
+ end
30
+ end
31
+
32
+ [:limit, :offset].each do |k|
33
+ define_method k do |value|
34
+ @args["_m_#{k}"] = value
35
+ @sections[k] = true
22
36
  self
23
37
  end
24
38
  end
25
39
 
26
- def to_sql
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,33 +82,29 @@ 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
98
+
52
99
  sql
53
100
  end
54
101
 
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
102
+ private def union_parameters(hash_args)
103
+ if hash_args
104
+ @args.merge(hash_args)
105
+ else
106
+ @args
107
+ end
67
108
  end
68
109
 
69
110
  end
70
-
@@ -4,7 +4,7 @@ module MiniSql
4
4
  class Connection
5
5
 
6
6
  def self.get(raw_connection, options = {})
7
- if (defined? ::PG::Connection) && (PG::Connection === raw_connection)
7
+ if (defined? ::PG::Connection) && (PG::Connection === raw_connection)
8
8
  Postgres::Connection.new(raw_connection, options)
9
9
  elsif (defined? ::ArJdbc)
10
10
  Postgres::Connection.new(raw_connection, options)
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniSql
2
4
  class DeserializerCache
3
5
  # method takes a raw result and converts to proper objects
@@ -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,31 +36,32 @@ 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
 
51
49
  def quote_val(value)
52
50
  case value
51
+ when String then "'#{conn.escape_string(value.to_s)}'"
52
+ when Numeric then value.to_s
53
+ when BigDecimal then value.to_s("F")
54
+ when Time then "'#{quoted_time(value)}'"
55
+ when Date then "'#{value.to_s}'"
56
+ when Symbol then "'#{conn.escape_string(value.to_s)}'"
57
+ when true then "true"
58
+ when false then "false"
59
+ when nil then "NULL"
60
+ when [] then "NULL"
53
61
  when Array
54
62
  value.map do |v|
55
63
  quote_val(v)
56
64
  end.join(', ')
57
- when String
58
- "'#{conn.escape_string(value.to_s)}'"
59
- when true then "true"
60
- when false then "false"
61
- when nil then "NULL"
62
- when BigDecimal then value.to_s("F")
63
- when Numeric then value.to_s
64
- when Date, Time then "'#{quoted_date(value)}'"
65
- when Symbol then "'#{conn.escape_string(value.to_s)}'"
66
65
  else raise TypeError, "can't quote #{value.class.name}"
67
66
  end
68
67
  end
@@ -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)
@@ -20,6 +26,10 @@ module MiniSql
20
26
  result.to_a
21
27
  end
22
28
 
29
+ def query_array(sql, *params)
30
+ run(sql, :array, params).to_a
31
+ end
32
+
23
33
  def exec(sql, *params)
24
34
  run(sql, :array, params)
25
35
  raw_connection.affected_rows
@@ -30,6 +40,11 @@ module MiniSql
30
40
  @deserializer_cache.materialize(result)
31
41
  end
32
42
 
43
+ def query_decorator(decorator, sql, *params)
44
+ result = run(sql, :array, params)
45
+ @deserializer_cache.materialize(result, decorator)
46
+ end
47
+
33
48
  def escape_string(str)
34
49
  raw_connection.escape(str)
35
50
  end
@@ -45,9 +60,9 @@ module MiniSql
45
60
  sql = param_encoder.encode(sql, *params)
46
61
  end
47
62
  raw_connection.query(
48
- sql,
49
- as: as,
50
- database_timezone: :utc,
63
+ sql,
64
+ as: as,
65
+ database_timezone: :utc,
51
66
  application_timezone: :utc,
52
67
  cast_booleans: true,
53
68
  cast: true,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniSql
2
4
  module Mysql
3
5
  class DeserializerCache
@@ -9,18 +11,22 @@ module MiniSql
9
11
  @max_size = max_size || DEFAULT_MAX_SIZE
10
12
  end
11
13
 
12
- def materialize(result)
13
- key = result.fields
14
+ def materialize(result, decorator_module = nil)
15
+ key = result.fields.join(',')
14
16
 
15
17
  # trivial fast LRU implementation
16
18
  materializer = @cache.delete(key)
17
19
  if materializer
18
20
  @cache[key] = materializer
19
21
  else
20
- materializer = @cache[key] = new_row_matrializer(result)
22
+ materializer = @cache[key] = new_row_materializer(result)
21
23
  @cache.shift if @cache.length > @max_size
22
24
  end
23
25
 
26
+ if decorator_module
27
+ materializer = materializer.decorated(decorator_module)
28
+ end
29
+
24
30
  result.map do |data|
25
31
  materializer.materialize(data)
26
32
  end
@@ -28,27 +34,19 @@ module MiniSql
28
34
 
29
35
  private
30
36
 
31
- def new_row_matrializer(result)
37
+ def new_row_materializer(result)
32
38
  fields = result.fields
33
39
 
34
40
  Class.new do
35
- attr_accessor(*fields)
41
+ extend MiniSql::Decoratable
42
+ include MiniSql::Result
36
43
 
37
- # AM serializer support
38
- alias :read_attribute_for_serialization :send
39
-
40
- def to_h
41
- r = {}
42
- instance_variables.each do |f|
43
- r[f.to_s.sub('@','').to_sym] = instance_variable_get(f)
44
- end
45
- r
46
- end
44
+ attr_accessor(*fields)
47
45
 
48
46
  instance_eval <<~RUBY
49
47
  def materialize(data)
50
48
  r = self.new
51
- #{col=-1; fields.map{|f| "r.#{f} = data[#{col+=1}]"}.join("; ")}
49
+ #{col = -1; fields.map { |f| "r.#{f} = data[#{col += 1}]" }.join("; ")}
52
50
  r
53
51
  end
54
52
  RUBY
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_sql/abstract/prepared_binds"
4
+
5
+ module MiniSql
6
+ module Mysql
7
+ class PreparedBinds < ::MiniSql::Abstract::PreparedBinds
8
+
9
+ def bind_output(i)
10
+ '?'
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_sql/abstract/prepared_cache"
4
+
5
+ module MiniSql
6
+ module Mysql
7
+ class PreparedCache < ::MiniSql::Abstract::PreparedCache
8
+
9
+ private
10
+
11
+ def alloc(sql)
12
+ @connection.prepare(sql)
13
+ end
14
+
15
+ def dealloc(statement)
16
+ statement.close unless statement.closed?
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniSql
4
+ module Mysql
5
+ class PreparedConnection < Connection
6
+
7
+ attr_reader :unprepared
8
+
9
+ def initialize(unprepared_connection, deserializer_cache)
10
+ @unprepared = unprepared_connection
11
+ @raw_connection = unprepared_connection.raw_connection
12
+ @deserializer_cache = deserializer_cache
13
+ @param_encoder = unprepared_connection.param_encoder
14
+
15
+ @prepared_cache = PreparedCache.new(@raw_connection)
16
+ @param_binder = PreparedBinds.new
17
+ end
18
+
19
+ def build(_)
20
+ raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
21
+ end
22
+
23
+ def prepared(condition = true)
24
+ condition ? self : @unprepared
25
+ end
26
+
27
+ private def run(sql, as, params)
28
+ prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
29
+ statement = @prepared_cache.prepare_statement(prepared_sql)
30
+ statement.execute(
31
+ *binds,
32
+ as: as,
33
+ database_timezone: :utc,
34
+ application_timezone: :utc,
35
+ cast_booleans: true,
36
+ cast: true,
37
+ cache_rows: true,
38
+ symbolize_keys: false
39
+ )
40
+ end
41
+
42
+ end
43
+ end
44
+ end