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
@@ -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)
@@ -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
@@ -40,12 +40,18 @@ module MiniSql
40
40
  @deserializer_cache = (args && args[:deserializer_cache]) || self.class.default_deserializer_cache
41
41
  @param_encoder = (args && args[:param_encoder]) || InlineParamEncoder.new(self)
42
42
  @type_map = args && args[:type_map]
43
+
44
+ @prepared = PreparedConnection.new(self, @deserializer_cache)
43
45
  end
44
46
 
45
47
  def type_map
46
48
  @type_map ||= self.class.type_map(raw_connection)
47
49
  end
48
50
 
51
+ def prepared(condition = true)
52
+ condition ? @prepared : self
53
+ end
54
+
49
55
  # Returns a flat array containing all results.
50
56
  # Note, if selecting multiple columns array will be flattened
51
57
  #
@@ -83,8 +89,8 @@ module MiniSql
83
89
  result = run(sql, params)
84
90
  result.type_map = type_map
85
91
  result.values
86
- ensure
87
- result.clear if result
92
+ ensure
93
+ result.clear if result
88
94
  end
89
95
 
90
96
  def query(sql, *params)
@@ -95,6 +101,72 @@ module MiniSql
95
101
  result.clear if result
96
102
  end
97
103
 
104
+ def query_each(sql, *params)
105
+ raise StandardError, "Please supply a block when calling query_each" if !block_given?
106
+ if params && params.length > 0
107
+ sql = param_encoder.encode(sql, *params)
108
+ end
109
+
110
+ raw_connection.send_query(sql)
111
+ raw_connection.set_single_row_mode
112
+
113
+ loop do
114
+ result = raw_connection.get_result
115
+ break if !result
116
+
117
+ result.check
118
+
119
+ if result.ntuples == 0
120
+ # skip, this happens at the end when we get totals
121
+ else
122
+ materializer ||= @deserializer_cache.materializer(result)
123
+ result.type_map = type_map
124
+ i = 0
125
+ # technically we should only get 1 row here
126
+ # but protect against future batching changes
127
+ while i < result.ntuples
128
+ yield materializer.materialize(result, i)
129
+ i += 1
130
+ end
131
+ end
132
+
133
+ result.clear
134
+ end
135
+ end
136
+
137
+ def query_each_hash(sql, *params)
138
+ raise StandardError, "Please supply a block when calling query_each_hash" if !block_given?
139
+ if params && params.length > 0
140
+ sql = param_encoder.encode(sql, *params)
141
+ end
142
+
143
+ raw_connection.send_query(sql)
144
+ raw_connection.set_single_row_mode
145
+
146
+ loop do
147
+ result = raw_connection.get_result
148
+ break if !result
149
+
150
+ result.check
151
+
152
+ if result.ntuples == 0
153
+ # skip, this happens at the end when we get totals
154
+ else
155
+ result.type_map = type_map
156
+ i = 0
157
+
158
+ # technically we should only get 1 row here
159
+ # but protect against future batching changes
160
+ while i < result.ntuples
161
+ yield result[i]
162
+ i += 1
163
+ end
164
+ end
165
+
166
+ result.clear
167
+ end
168
+ end
169
+
98
170
  def query_decorator(decorator, sql, *params)
99
171
  result = run(sql, params)
100
172
  result.type_map = type_map
@@ -11,21 +11,37 @@ module MiniSql
11
11
  @max_size = max_size || DEFAULT_MAX_SIZE
12
12
  end
13
13
 
14
+ def materializer(result)
15
+ key = result.fields.join(',')
16
+
17
+ materializer = @cache.delete(key)
18
+ if materializer
19
+ @cache[key] = materializer
20
+ else
21
+ materializer = @cache[key] = new_row_materializer(result)
22
+ @cache.shift if @cache.length > @max_size
23
+ end
24
+
25
+ materializer
26
+ end
27
+
14
28
  def materialize(result, decorator_module = nil)
15
29
  return [] if result.ntuples == 0
16
30
 
17
- key = result.fields
31
+ key = result.fields.join(',')
18
32
 
19
33
  # trivial fast LRU implementation
20
34
  materializer = @cache.delete(key)
21
35
  if materializer
22
36
  @cache[key] = materializer
23
37
  else
24
- materializer = @cache[key] = new_row_matrializer(result)
38
+ materializer = @cache[key] = new_row_materializer(result)
25
39
  @cache.shift if @cache.length > @max_size
26
40
  end
27
41
 
28
- materializer.include(decorator_module) if decorator_module
42
+ if decorator_module
43
+ materializer = materializer.decorated(decorator_module)
44
+ end
29
45
 
30
46
  i = 0
31
47
  r = []
@@ -39,22 +55,23 @@ module MiniSql
39
55
 
40
56
  private
41
57
 
42
- def new_row_matrializer(result)
58
+ def new_row_materializer(result)
43
59
  fields = result.fields
44
60
 
45
- Class.new do
46
- attr_accessor(*fields)
61
+ i = 0
62
+ while i < fields.length
63
+ # special handling for unamed column
64
+ if fields[i] == "?column?"
65
+ fields[i] = "column#{i}"
66
+ end
67
+ i += 1
68
+ end
47
69
 
48
- # AM serializer support
49
- alias :read_attribute_for_serialization :send
70
+ Class.new do
71
+ extend MiniSql::Decoratable
72
+ include MiniSql::Result
50
73
 
51
- def to_h
52
- r = {}
53
- instance_variables.each do |f|
54
- r[f.to_s.sub('@', '').to_sym] = instance_variable_get(f)
55
- end
56
- r
57
- end
74
+ attr_accessor(*fields)
58
75
 
59
76
  instance_eval <<~RUBY
60
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)