mini_sql 0.2.5 → 1.1.1

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