mini_sql 1.0 → 1.1.3

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