mini_sql 0.2.4 → 1.1.0

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