mini_sql 1.0 → 1.1.3

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/CHANGELOG +20 -0
  4. data/README.md +36 -0
  5. data/bench/builder_perf.rb +138 -0
  6. data/bench/decorator_perf.rb +143 -0
  7. data/bench/mini_sql_methods_perf.rb +80 -0
  8. data/bench/prepared_perf.rb +59 -0
  9. data/bench/shared/generate_data.rb +133 -0
  10. data/bench/topic_perf.rb +21 -327
  11. data/bench/topic_wide_perf.rb +92 -0
  12. data/lib/mini_sql.rb +17 -8
  13. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  14. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  15. data/lib/mini_sql/builder.rb +63 -30
  16. data/lib/mini_sql/inline_param_encoder.rb +4 -5
  17. data/lib/mini_sql/mysql/connection.rb +13 -3
  18. data/lib/mini_sql/mysql/deserializer_cache.rb +3 -3
  19. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  20. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  21. data/lib/mini_sql/mysql/prepared_connection.rb +47 -0
  22. data/lib/mini_sql/postgres/connection.rb +21 -7
  23. data/lib/mini_sql/postgres/deserializer_cache.rb +5 -5
  24. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  25. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  26. data/lib/mini_sql/postgres/prepared_connection.rb +39 -0
  27. data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
  28. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +3 -3
  29. data/lib/mini_sql/result.rb +10 -0
  30. data/lib/mini_sql/serializer.rb +29 -15
  31. data/lib/mini_sql/sqlite/connection.rb +14 -2
  32. data/lib/mini_sql/sqlite/deserializer_cache.rb +3 -3
  33. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  34. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  35. data/lib/mini_sql/sqlite/prepared_connection.rb +43 -0
  36. data/lib/mini_sql/version.rb +1 -1
  37. metadata +20 -3
@@ -12,14 +12,14 @@ 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
 
@@ -34,7 +34,7 @@ module MiniSql
34
34
 
35
35
  private
36
36
 
37
- def new_row_matrializer(result)
37
+ def new_row_materializer(result)
38
38
  fields = result.fields
39
39
 
40
40
  Class.new do
@@ -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,47 @@
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)
10
+ @unprepared = unprepared_connection
11
+ @raw_connection = unprepared_connection.raw_connection
12
+ @param_encoder = unprepared_connection.param_encoder
13
+
14
+ @prepared_cache = PreparedCache.new(@raw_connection)
15
+ @param_binder = PreparedBinds.new
16
+ end
17
+
18
+ def build(_)
19
+ raise 'Builder can not be called on prepared connections, instead of `::MINI_SQL.prepared.build(sql).query` use `::MINI_SQL.build(sql).prepared.query`'
20
+ end
21
+
22
+ def prepared(condition = true)
23
+ condition ? self : @unprepared
24
+ end
25
+
26
+ def deserializer_cache
27
+ @unprepared.deserializer_cache
28
+ end
29
+
30
+ private def run(sql, as, params)
31
+ prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
32
+ statement = @prepared_cache.prepare_statement(prepared_sql)
33
+ statement.execute(
34
+ *binds,
35
+ as: as,
36
+ database_timezone: :utc,
37
+ application_timezone: :utc,
38
+ cast_booleans: true,
39
+ cast: true,
40
+ cache_rows: true,
41
+ symbolize_keys: false
42
+ )
43
+ end
44
+
45
+ end
46
+ end
47
+ 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, :deserializer_cache
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)
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
  #
@@ -90,14 +98,16 @@ module MiniSql
90
98
  def query(sql, *params)
91
99
  result = run(sql, params)
92
100
  result.type_map = type_map
93
- @deserializer_cache.materialize(result)
101
+ deserializer_cache.materialize(result)
94
102
  ensure
95
103
  result.clear if result
96
104
  end
97
105
 
98
106
  def query_each(sql, *params)
99
107
  raise StandardError, "Please supply a block when calling query_each" if !block_given?
100
- sql = param_encoder.encode(sql, *params)
108
+ if params && params.length > 0
109
+ sql = param_encoder.encode(sql, *params)
110
+ end
101
111
 
102
112
  raw_connection.send_query(sql)
103
113
  raw_connection.set_single_row_mode
@@ -111,7 +121,7 @@ module MiniSql
111
121
  if result.ntuples == 0
112
122
  # skip, this happens at the end when we get totals
113
123
  else
114
- materializer ||= @deserializer_cache.materializer(result)
124
+ materializer ||= deserializer_cache.materializer(result)
115
125
  result.type_map = type_map
116
126
  i = 0
117
127
  # technically we should only get 1 row here
@@ -128,7 +138,9 @@ module MiniSql
128
138
 
129
139
  def query_each_hash(sql, *params)
130
140
  raise StandardError, "Please supply a block when calling query_each_hash" if !block_given?
131
- sql = param_encoder.encode(sql, *params)
141
+ if params && params.length > 0
142
+ sql = param_encoder.encode(sql, *params)
143
+ end
132
144
 
133
145
  raw_connection.send_query(sql)
134
146
  raw_connection.set_single_row_mode
@@ -160,7 +172,7 @@ module MiniSql
160
172
  def query_decorator(decorator, sql, *params)
161
173
  result = run(sql, params)
162
174
  result.type_map = type_map
163
- @deserializer_cache.materialize(result, decorator)
175
+ deserializer_cache.materialize(result, decorator)
164
176
  ensure
165
177
  result.clear if result
166
178
  end
@@ -191,7 +203,9 @@ module MiniSql
191
203
  private
192
204
 
193
205
  def run(sql, params)
194
- sql = param_encoder.encode(sql, *params)
206
+ if params && params.length > 0
207
+ sql = param_encoder.encode(sql, *params)
208
+ end
195
209
  raw_connection.async_exec(sql)
196
210
  end
197
211
 
@@ -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,14 +28,14 @@ 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
 
@@ -55,7 +55,7 @@ module MiniSql
55
55
 
56
56
  private
57
57
 
58
- def new_row_matrializer(result)
58
+ def new_row_materializer(result)
59
59
  fields = result.fields
60
60
 
61
61
  i = 0
@@ -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,39 @@
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)
10
+ @unprepared = unprepared_connection
11
+ @raw_connection = unprepared_connection.raw_connection
12
+ @type_map = unprepared_connection.type_map
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
+ def deserializer_cache
28
+ @unprepared.deserializer_cache
29
+ end
30
+
31
+ private def run(sql, params)
32
+ prepared_sql, binds, _bind_names = @param_binder.bind(sql, *params)
33
+ prepare_statement_key = @prepared_cache.prepare_statement(prepared_sql)
34
+ raw_connection.exec_prepared(prepare_statement_key, binds)
35
+ end
36
+
37
+ end
38
+ end
39
+ 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)
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,14 +15,14 @@ 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
 
@@ -44,7 +44,7 @@ module MiniSql
44
44
 
45
45
  private
46
46
 
47
- def new_row_matrializer(result)
47
+ def new_row_materializer(result)
48
48
  fields = result.fields
49
49
 
50
50
  Class.new do
@@ -16,5 +16,15 @@ module MiniSql
16
16
  def values
17
17
  instance_variables.map { |f| instance_variable_get(f) }
18
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
19
29
  end
20
30
  end
@@ -1,26 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniSql
4
- module Serializer
4
+ class Serializer < Array
5
5
  MAX_CACHE_SIZE = 500
6
6
 
7
- def self.to_json(result)
8
- wrapper =
9
- if result.length == 0
10
- {}
11
- else
12
- {
13
- "decorator" => result[0].class.decorator.to_s,
14
- "fields" => result[0].to_h.keys,
15
- "data" => result.map(&:values),
16
- }
17
- end
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
18
 
19
- JSON.generate(wrapper)
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
20
35
  end
21
36
 
22
- def self.from_json(json)
23
- wrapper = JSON.parse(json)
37
+ def self.materialize(wrapper)
24
38
  if !wrapper["data"]
25
39
  []
26
40
  else
@@ -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)
17
+ else
18
+ self
19
+ end
20
+ end
21
+
14
22
  def query_single(sql, *params)
15
23
  # a bit lazy can be optimized
16
24
  run(sql, *params).flatten!
@@ -63,10 +71,14 @@ module MiniSql
63
71
  private
64
72
 
65
73
  def run(sql, *params)
66
- sql = param_encoder.encode(sql, *params)
74
+ if params && params.length > 0
75
+ sql = param_encoder.encode(sql, *params)
76
+ end
67
77
  if block_given?
68
78
  stmt = SQLite3::Statement.new(raw_connection, sql)
69
- yield stmt.execute
79
+ result = yield stmt.execute
80
+ stmt.close
81
+ result
70
82
  else
71
83
  raw_connection.execute(sql)
72
84
  end