mini_sql 0.3 → 1.1.2

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 (41) 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 +36 -0
  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/decoratable.rb +22 -0
  18. data/lib/mini_sql/inline_param_encoder.rb +4 -5
  19. data/lib/mini_sql/mysql/connection.rb +8 -0
  20. data/lib/mini_sql/mysql/deserializer_cache.rb +9 -15
  21. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  22. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  23. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  24. data/lib/mini_sql/postgres/connection.rb +10 -2
  25. data/lib/mini_sql/postgres/deserializer_cache.rb +11 -17
  26. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  27. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  28. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  29. data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
  30. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +10 -14
  31. data/lib/mini_sql/result.rb +30 -0
  32. data/lib/mini_sql/serializer.rb +84 -0
  33. data/lib/mini_sql/sqlite/connection.rb +11 -1
  34. data/lib/mini_sql/sqlite/deserializer_cache.rb +9 -15
  35. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  36. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  37. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  38. data/lib/mini_sql/version.rb +1 -1
  39. data/mini_sql.gemspec +5 -4
  40. metadata +42 -8
  41. data/.travis.yml +0 -28
@@ -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, @deserializer_cache)
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
@@ -12,18 +12,20 @@ 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
 
26
- materializer.include(decorator_module) if decorator_module
26
+ if decorator_module
27
+ materializer = materializer.decorated(decorator_module)
28
+ end
27
29
 
28
30
  result.map do |data|
29
31
  materializer.materialize(data)
@@ -32,22 +34,14 @@ module MiniSql
32
34
 
33
35
  private
34
36
 
35
- def new_row_matrializer(result)
37
+ def new_row_materializer(result)
36
38
  fields = result.fields
37
39
 
38
40
  Class.new do
39
- attr_accessor(*fields)
40
-
41
- # AM serializer support
42
- alias :read_attribute_for_serialization :send
41
+ extend MiniSql::Decoratable
42
+ include MiniSql::Result
43
43
 
44
- def to_h
45
- r = {}
46
- instance_variables.each do |f|
47
- r[f.to_s.sub('@', '').to_sym] = instance_variable_get(f)
48
- end
49
- r
50
- end
44
+ attr_accessor(*fields)
51
45
 
52
46
  instance_eval <<~RUBY
53
47
  def materialize(data)
@@ -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
@@ -3,7 +3,7 @@
3
3
  module MiniSql
4
4
  module Postgres
5
5
  class Connection < MiniSql::Connection
6
- attr_reader :raw_connection, :type_map, :param_encoder
6
+ attr_reader :raw_connection, :param_encoder
7
7
 
8
8
  def self.default_deserializer_cache
9
9
  @deserializer_cache ||= DeserializerCache.new
@@ -46,6 +46,14 @@ module MiniSql
46
46
  @type_map ||= self.class.type_map(raw_connection)
47
47
  end
48
48
 
49
+ def prepared(condition = true)
50
+ if condition
51
+ @prepared ||= PreparedConnection.new(self, @deserializer_cache)
52
+ else
53
+ self
54
+ end
55
+ end
56
+
49
57
  # Returns a flat array containing all results.
50
58
  # Note, if selecting multiple columns array will be flattened
51
59
  #
@@ -84,7 +92,7 @@ module MiniSql
84
92
  result.type_map = type_map
85
93
  result.values
86
94
  ensure
87
- result.clear if result
95
+ result.clear if result
88
96
  end
89
97
 
90
98
  def query(sql, *params)
@@ -12,13 +12,13 @@ module MiniSql
12
12
  end
13
13
 
14
14
  def materializer(result)
15
- key = result.fields
15
+ key = result.fields.join(',')
16
16
 
17
17
  materializer = @cache.delete(key)
18
18
  if materializer
19
19
  @cache[key] = materializer
20
20
  else
21
- materializer = @cache[key] = new_row_matrializer(result)
21
+ materializer = @cache[key] = new_row_materializer(result)
22
22
  @cache.shift if @cache.length > @max_size
23
23
  end
24
24
 
@@ -28,18 +28,20 @@ module MiniSql
28
28
  def materialize(result, decorator_module = nil)
29
29
  return [] if result.ntuples == 0
30
30
 
31
- key = result.fields
31
+ key = result.fields.join(',')
32
32
 
33
33
  # trivial fast LRU implementation
34
34
  materializer = @cache.delete(key)
35
35
  if materializer
36
36
  @cache[key] = materializer
37
37
  else
38
- materializer = @cache[key] = new_row_matrializer(result)
38
+ materializer = @cache[key] = new_row_materializer(result)
39
39
  @cache.shift if @cache.length > @max_size
40
40
  end
41
41
 
42
- materializer.include(decorator_module) if decorator_module
42
+ if decorator_module
43
+ materializer = materializer.decorated(decorator_module)
44
+ end
43
45
 
44
46
  i = 0
45
47
  r = []
@@ -53,7 +55,7 @@ module MiniSql
53
55
 
54
56
  private
55
57
 
56
- def new_row_matrializer(result)
58
+ def new_row_materializer(result)
57
59
  fields = result.fields
58
60
 
59
61
  i = 0
@@ -66,18 +68,10 @@ module MiniSql
66
68
  end
67
69
 
68
70
  Class.new do
69
- attr_accessor(*fields)
70
-
71
- # AM serializer support
72
- alias :read_attribute_for_serialization :send
71
+ extend MiniSql::Decoratable
72
+ include MiniSql::Result
73
73
 
74
- def to_h
75
- r = {}
76
- instance_variables.each do |f|
77
- r[f.to_s.sub('@', '').to_sym] = instance_variable_get(f)
78
- end
79
- r
80
- end
74
+ attr_accessor(*fields)
81
75
 
82
76
  instance_eval <<~RUBY
83
77
  def materialize(pg_result, index)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_sql/abstract/prepared_binds"
4
+
5
+ module MiniSql
6
+ module Postgres
7
+ class PreparedBinds < ::MiniSql::Abstract::PreparedBinds
8
+
9
+ def bind_output(i)
10
+ "$#{i}"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_sql/abstract/prepared_cache"
4
+
5
+ module MiniSql
6
+ module Postgres
7
+ class PreparedCache < ::MiniSql::Abstract::PreparedCache
8
+
9
+ private
10
+
11
+ def alloc(sql)
12
+ alloc_key = next_key
13
+ @connection.prepare(alloc_key, sql)
14
+
15
+ alloc_key
16
+ end
17
+
18
+ def dealloc(key)
19
+ @connection.query "DEALLOCATE #{key}" if @connection.status == PG::CONNECTION_OK
20
+ rescue PG::Error
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniSql
4
+ module Postgres
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
+ @type_map = unprepared_connection.type_map
14
+ @param_encoder = unprepared_connection.param_encoder
15
+
16
+ @prepared_cache = PreparedCache.new(@raw_connection)
17
+ @param_binder = PreparedBinds.new
18
+ end
19
+
20
+ def build(_)
21
+ raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
22
+ end
23
+
24
+ def prepared(condition = true)
25
+ condition ? self : @unprepared
26
+ end
27
+
28
+ private def run(sql, params)
29
+ prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
30
+ prepare_statement_key = @prepared_cache.prepare_statement(prepared_sql)
31
+ raw_connection.exec_prepared(prepare_statement_key, binds)
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -89,7 +89,9 @@ module MiniSql
89
89
  private
90
90
 
91
91
  def run(sql, params)
92
- sql = param_encoder.encode(sql, *params) if params && params.length > 0
92
+ if params && params.length > 0
93
+ sql = param_encoder.encode(sql, *params)
94
+ end
93
95
  conn = raw_connection
94
96
  conn.typemap = self.class.typemap
95
97
  conn.execute(sql)
@@ -15,19 +15,23 @@ module MiniSql
15
15
 
16
16
  return [] if result.ntuples == 0
17
17
 
18
- key = result.fields
18
+ key = result.fields.join(',')
19
19
 
20
20
  # trivial fast LRU implementation
21
21
  materializer = @cache.delete(key)
22
22
  if materializer
23
23
  @cache[key] = materializer
24
24
  else
25
- materializer = @cache[key] = new_row_matrializer(result)
25
+ materializer = @cache[key] = new_row_materializer(result)
26
26
  @cache.shift if @cache.length > @max_size
27
27
  end
28
28
 
29
29
  materializer.include(decorator_module) if decorator_module
30
30
 
31
+ if decorator_module
32
+ materializer = materializer.decorated(decorator_module)
33
+ end
34
+
31
35
  i = 0
32
36
  r = []
33
37
  # quicker loop
@@ -40,22 +44,14 @@ module MiniSql
40
44
 
41
45
  private
42
46
 
43
- def new_row_matrializer(result)
47
+ def new_row_materializer(result)
44
48
  fields = result.fields
45
49
 
46
50
  Class.new do
47
- attr_accessor(*fields)
51
+ extend MiniSql::Decoratable
52
+ include MiniSql::Result
48
53
 
49
- # AM serializer support
50
- alias :read_attribute_for_serialization :send
51
-
52
- def to_h
53
- r = {}
54
- instance_variables.each do |f|
55
- r[f.to_s.sub('@', '').to_sym] = instance_variable_get(f)
56
- end
57
- r
58
- end
54
+ attr_accessor(*fields)
59
55
 
60
56
  instance_eval <<~RUBY
61
57
  def materialize(pg_result, index)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniSql
4
+ module Result
5
+ # AM serializer support
6
+ alias :read_attribute_for_serialization :send
7
+
8
+ def to_h
9
+ r = {}
10
+ instance_variables.each do |f|
11
+ r[f.to_s.delete_prefix('@').to_sym] = instance_variable_get(f)
12
+ end
13
+ r
14
+ end
15
+
16
+ def values
17
+ instance_variables.map { |f| instance_variable_get(f) }
18
+ end
19
+
20
+ def ==(other_result)
21
+ self.class.decorator == other_result.class.decorator &&
22
+ self.instance_variables == other_result.instance_variables &&
23
+ self.values == other_result.values
24
+ end
25
+
26
+ def eql?(other_result)
27
+ self == other_result
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniSql
4
+ class Serializer < Array
5
+ MAX_CACHE_SIZE = 500
6
+
7
+ def initialize(result)
8
+ replace(result)
9
+ end
10
+
11
+ def self.marshallable(result)
12
+ new(result)
13
+ end
14
+
15
+ def marshal_dump
16
+ serialize
17
+ end
18
+
19
+ def marshal_load(wrapper)
20
+ replace self.class.materialize(wrapper)
21
+ end
22
+
23
+ private
24
+
25
+ def serialize
26
+ if length == 0
27
+ {}
28
+ else
29
+ {
30
+ "decorator" => first.class.decorator.to_s,
31
+ "fields" => first.to_h.keys,
32
+ "data" => map(&:values),
33
+ }
34
+ end
35
+ end
36
+
37
+ def self.materialize(wrapper)
38
+ if !wrapper["data"]
39
+ []
40
+ else
41
+ materializer = cached_materializer(wrapper['fields'], wrapper['decorator'])
42
+ wrapper["data"].map do |row|
43
+ materializer.materialize(row)
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.cached_materializer(fields, decorator_module = nil)
49
+ @cache ||= {}
50
+ key = fields
51
+ m = @cache.delete(key)
52
+ if m
53
+ @cache[key] = m
54
+ else
55
+ m = @cache[key] = materializer(fields)
56
+ @cache.shift if @cache.length > MAX_CACHE_SIZE
57
+ end
58
+
59
+ if decorator_module && decorator_module.length > 0
60
+ decorator = Kernel.const_get(decorator_module)
61
+ m = m.decorated(decorator)
62
+ end
63
+
64
+ m
65
+ end
66
+
67
+ def self.materializer(fields)
68
+ Class.new do
69
+ extend MiniSql::Decoratable
70
+ include MiniSql::Result
71
+
72
+ attr_accessor(*fields)
73
+
74
+ instance_eval <<~RUBY
75
+ def materialize(values)
76
+ r = self.new
77
+ #{col = -1; fields.map { |f| "r.#{f} = values[#{col += 1}]" }.join("; ")}
78
+ r
79
+ end
80
+ RUBY
81
+ end
82
+ end
83
+ end
84
+ end