sequel 3.33.0 → 3.34.0

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 (152) hide show
  1. data/CHANGELOG +140 -0
  2. data/Rakefile +7 -0
  3. data/bin/sequel +22 -2
  4. data/doc/dataset_basics.rdoc +1 -1
  5. data/doc/mass_assignment.rdoc +3 -1
  6. data/doc/querying.rdoc +28 -4
  7. data/doc/reflection.rdoc +23 -3
  8. data/doc/release_notes/3.34.0.txt +671 -0
  9. data/doc/schema_modification.rdoc +18 -2
  10. data/doc/virtual_rows.rdoc +49 -0
  11. data/lib/sequel/adapters/do/mysql.rb +0 -5
  12. data/lib/sequel/adapters/ibmdb.rb +9 -4
  13. data/lib/sequel/adapters/jdbc.rb +9 -4
  14. data/lib/sequel/adapters/jdbc/h2.rb +8 -2
  15. data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
  16. data/lib/sequel/adapters/jdbc/postgresql.rb +43 -0
  17. data/lib/sequel/adapters/jdbc/sqlite.rb +19 -0
  18. data/lib/sequel/adapters/mock.rb +24 -3
  19. data/lib/sequel/adapters/mysql.rb +29 -50
  20. data/lib/sequel/adapters/mysql2.rb +13 -28
  21. data/lib/sequel/adapters/oracle.rb +8 -2
  22. data/lib/sequel/adapters/postgres.rb +115 -20
  23. data/lib/sequel/adapters/shared/db2.rb +1 -1
  24. data/lib/sequel/adapters/shared/mssql.rb +14 -3
  25. data/lib/sequel/adapters/shared/mysql.rb +59 -11
  26. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
  27. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  28. data/lib/sequel/adapters/shared/postgres.rb +127 -30
  29. data/lib/sequel/adapters/shared/sqlite.rb +55 -38
  30. data/lib/sequel/adapters/sqlite.rb +9 -3
  31. data/lib/sequel/adapters/swift.rb +2 -2
  32. data/lib/sequel/adapters/swift/mysql.rb +0 -5
  33. data/lib/sequel/adapters/swift/postgres.rb +10 -0
  34. data/lib/sequel/ast_transformer.rb +4 -0
  35. data/lib/sequel/connection_pool.rb +8 -0
  36. data/lib/sequel/connection_pool/sharded_single.rb +5 -0
  37. data/lib/sequel/connection_pool/sharded_threaded.rb +17 -0
  38. data/lib/sequel/connection_pool/single.rb +5 -0
  39. data/lib/sequel/connection_pool/threaded.rb +14 -0
  40. data/lib/sequel/core.rb +24 -3
  41. data/lib/sequel/database/connecting.rb +24 -14
  42. data/lib/sequel/database/dataset_defaults.rb +1 -0
  43. data/lib/sequel/database/misc.rb +16 -25
  44. data/lib/sequel/database/query.rb +20 -2
  45. data/lib/sequel/database/schema_generator.rb +2 -2
  46. data/lib/sequel/database/schema_methods.rb +120 -23
  47. data/lib/sequel/dataset/actions.rb +91 -18
  48. data/lib/sequel/dataset/features.rb +5 -0
  49. data/lib/sequel/dataset/prepared_statements.rb +6 -2
  50. data/lib/sequel/dataset/sql.rb +68 -51
  51. data/lib/sequel/extensions/_pretty_table.rb +79 -0
  52. data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +18 -13
  53. data/lib/sequel/extensions/migration.rb +4 -0
  54. data/lib/sequel/extensions/null_dataset.rb +90 -0
  55. data/lib/sequel/extensions/pg_array.rb +460 -0
  56. data/lib/sequel/extensions/pg_array_ops.rb +220 -0
  57. data/lib/sequel/extensions/pg_auto_parameterize.rb +174 -0
  58. data/lib/sequel/extensions/pg_hstore.rb +296 -0
  59. data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
  60. data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
  61. data/lib/sequel/extensions/pretty_table.rb +5 -71
  62. data/lib/sequel/extensions/query_literals.rb +79 -0
  63. data/lib/sequel/extensions/schema_caching.rb +76 -0
  64. data/lib/sequel/extensions/schema_dumper.rb +227 -31
  65. data/lib/sequel/extensions/select_remove.rb +35 -0
  66. data/lib/sequel/extensions/sql_expr.rb +4 -110
  67. data/lib/sequel/extensions/to_dot.rb +1 -1
  68. data/lib/sequel/model.rb +11 -2
  69. data/lib/sequel/model/associations.rb +35 -7
  70. data/lib/sequel/model/base.rb +159 -36
  71. data/lib/sequel/no_core_ext.rb +2 -0
  72. data/lib/sequel/plugins/caching.rb +25 -18
  73. data/lib/sequel/plugins/composition.rb +1 -1
  74. data/lib/sequel/plugins/hook_class_methods.rb +1 -1
  75. data/lib/sequel/plugins/identity_map.rb +11 -3
  76. data/lib/sequel/plugins/instance_filters.rb +10 -0
  77. data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
  78. data/lib/sequel/plugins/nested_attributes.rb +4 -3
  79. data/lib/sequel/plugins/prepared_statements.rb +3 -1
  80. data/lib/sequel/plugins/prepared_statements_associations.rb +5 -1
  81. data/lib/sequel/plugins/schema.rb +7 -2
  82. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  83. data/lib/sequel/plugins/static_cache.rb +99 -0
  84. data/lib/sequel/plugins/validation_class_methods.rb +1 -1
  85. data/lib/sequel/sql.rb +417 -7
  86. data/lib/sequel/version.rb +1 -1
  87. data/spec/adapters/firebird_spec.rb +1 -1
  88. data/spec/adapters/mssql_spec.rb +12 -15
  89. data/spec/adapters/mysql_spec.rb +81 -23
  90. data/spec/adapters/postgres_spec.rb +444 -77
  91. data/spec/adapters/spec_helper.rb +2 -0
  92. data/spec/adapters/sqlite_spec.rb +8 -8
  93. data/spec/core/connection_pool_spec.rb +85 -0
  94. data/spec/core/database_spec.rb +29 -5
  95. data/spec/core/dataset_spec.rb +171 -3
  96. data/spec/core/expression_filters_spec.rb +364 -0
  97. data/spec/core/mock_adapter_spec.rb +17 -3
  98. data/spec/core/schema_spec.rb +133 -0
  99. data/spec/extensions/association_dependencies_spec.rb +13 -13
  100. data/spec/extensions/caching_spec.rb +26 -3
  101. data/spec/extensions/class_table_inheritance_spec.rb +2 -2
  102. data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +23 -94
  103. data/spec/extensions/force_encoding_spec.rb +4 -2
  104. data/spec/extensions/hook_class_methods_spec.rb +5 -2
  105. data/spec/extensions/identity_map_spec.rb +17 -0
  106. data/spec/extensions/instance_filters_spec.rb +1 -1
  107. data/spec/extensions/lazy_attributes_spec.rb +2 -2
  108. data/spec/extensions/list_spec.rb +4 -4
  109. data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
  110. data/spec/extensions/migration_spec.rb +6 -2
  111. data/spec/extensions/nested_attributes_spec.rb +20 -0
  112. data/spec/extensions/null_dataset_spec.rb +85 -0
  113. data/spec/extensions/optimistic_locking_spec.rb +2 -2
  114. data/spec/extensions/pg_array_ops_spec.rb +105 -0
  115. data/spec/extensions/pg_array_spec.rb +196 -0
  116. data/spec/extensions/pg_auto_parameterize_spec.rb +64 -0
  117. data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
  118. data/spec/extensions/pg_hstore_spec.rb +195 -0
  119. data/spec/extensions/pg_statement_cache_spec.rb +209 -0
  120. data/spec/extensions/prepared_statements_spec.rb +4 -0
  121. data/spec/extensions/pretty_table_spec.rb +6 -0
  122. data/spec/extensions/query_literals_spec.rb +168 -0
  123. data/spec/extensions/schema_caching_spec.rb +41 -0
  124. data/spec/extensions/schema_dumper_spec.rb +231 -11
  125. data/spec/extensions/schema_spec.rb +14 -2
  126. data/spec/extensions/select_remove_spec.rb +38 -0
  127. data/spec/extensions/sharding_spec.rb +6 -6
  128. data/spec/extensions/skip_create_refresh_spec.rb +1 -1
  129. data/spec/extensions/spec_helper.rb +2 -1
  130. data/spec/extensions/sql_expr_spec.rb +28 -19
  131. data/spec/extensions/static_cache_spec.rb +145 -0
  132. data/spec/extensions/touch_spec.rb +1 -1
  133. data/spec/extensions/typecast_on_load_spec.rb +9 -1
  134. data/spec/integration/associations_test.rb +6 -6
  135. data/spec/integration/database_test.rb +1 -1
  136. data/spec/integration/dataset_test.rb +89 -26
  137. data/spec/integration/migrator_test.rb +2 -3
  138. data/spec/integration/model_test.rb +3 -3
  139. data/spec/integration/plugin_test.rb +85 -22
  140. data/spec/integration/prepared_statement_test.rb +28 -8
  141. data/spec/integration/schema_test.rb +78 -7
  142. data/spec/integration/spec_helper.rb +1 -0
  143. data/spec/integration/timezone_test.rb +1 -1
  144. data/spec/integration/transaction_test.rb +4 -6
  145. data/spec/integration/type_test.rb +2 -2
  146. data/spec/model/associations_spec.rb +94 -8
  147. data/spec/model/base_spec.rb +4 -4
  148. data/spec/model/hooks_spec.rb +2 -2
  149. data/spec/model/model_spec.rb +19 -7
  150. data/spec/model/record_spec.rb +135 -58
  151. data/spec/model/spec_helper.rb +1 -0
  152. metadata +35 -7
@@ -0,0 +1,259 @@
1
+ # The pg_hstore_ops extension adds support to Sequel's DSL to make
2
+ # it easier to call PostgreSQL hstore functions and operators. The
3
+ # most common usage is taking an object that represents an SQL
4
+ # expression (such as a :symbol), and calling #hstore on it:
5
+ #
6
+ # h = :hstore_column.hstore
7
+ #
8
+ # This creates a Sequel::Postgres::HStoreOp object that can be used
9
+ # for easier querying:
10
+ #
11
+ # h - 'a' # hstore_column - 'a'
12
+ # h['a'] # hstore_column -> 'a'
13
+ #
14
+ # h.concat(:other_hstore_column) # ||
15
+ # h.has_key?('a') # ?
16
+ # h.contain_all(:array_column) # ?&
17
+ # h.contain_any(:array_column) # ?|
18
+ # h.contains(:other_hstore_column) # @>
19
+ # h.contained_by(:other_hstore_column) # <@
20
+ #
21
+ # h.defined # defined(hstore_column)
22
+ # h.delete('a') # delete(hstore_column, 'a')
23
+ # h.each # each(hstore_column)
24
+ # h.keys # akeys(hstore_column)
25
+ # h.populate(:a) # populate_record(a, hstore_column)
26
+ # h.record_set(:a) # (a #= hstore_column)
27
+ # h.skeys # skeys(hstore_column)
28
+ # h.slice(:a) # slice(hstore_column, a)
29
+ # h.svals # svals(hstore_column)
30
+ # h.to_array # hstore_to_array(hstore_column)
31
+ # h.to_matrix # hstore_to_matrix(hstore_column)
32
+ # h.values # avals(hstore_column)
33
+ #
34
+ # See the PostgreSQL hstore function and operator documentation for more
35
+ # details on what these functions and operators do.
36
+ #
37
+ # If you are also using the pg_hstore extension, you should load it before
38
+ # loading this extension. Doing so will allow you to use HStore#op to get
39
+ # an HStoreOp, allowing you to perform hstore operations on hstore literals.
40
+
41
+ module Sequel
42
+ module Postgres
43
+ # The HStoreOp class is a simple container for a single object that
44
+ # defines methods that yield Sequel expression objects representing
45
+ # PostgreSQL hstore operators and functions.
46
+ #
47
+ # In the method documentation examples, assume that:
48
+ #
49
+ # hstore_op = :hstore.hstore
50
+ class HStoreOp < Sequel::SQL::Wrapper
51
+ CONCAT = ["(".freeze, " || ".freeze, ")".freeze].freeze
52
+ CONTAIN_ALL = ["(".freeze, " ?& ".freeze, ")".freeze].freeze
53
+ CONTAIN_ANY = ["(".freeze, " ?| ".freeze, ")".freeze].freeze
54
+ CONTAINS = ["(".freeze, " @> ".freeze, ")".freeze].freeze
55
+ CONTAINED_BY = ["(".freeze, " <@ ".freeze, ")".freeze].freeze
56
+ HAS_KEY = ["(".freeze, " ? ".freeze, ")".freeze].freeze
57
+ LOOKUP = ["(".freeze, " -> ".freeze, ")".freeze].freeze
58
+ RECORD_SET = ["(".freeze, " #= ".freeze, ")".freeze].freeze
59
+
60
+ # Delete entries from an hstore using the subtraction operator:
61
+ #
62
+ # hstore_op - 'a' # (hstore - 'a')
63
+ def -(other)
64
+ HStoreOp.new(super)
65
+ end
66
+
67
+ # Lookup the value for the given key in an hstore:
68
+ #
69
+ # hstore_op['a'] # (hstore -> 'a')
70
+ def [](key)
71
+ Sequel::SQL::StringExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(LOOKUP, [value, key]))
72
+ end
73
+
74
+ # Check if the receiver contains all of the keys in the given array:
75
+ #
76
+ # hstore_op.contain_all(:a) # (hstore ?& a)
77
+ def contain_all(other)
78
+ bool_op(CONTAIN_ALL, other)
79
+ end
80
+
81
+ # Check if the receiver contains any of the keys in the given array:
82
+ #
83
+ # hstore_op.contain_any(:a) # (hstore ?| a)
84
+ def contain_any(other)
85
+ bool_op(CONTAIN_ANY, other)
86
+ end
87
+
88
+ # Check if the receiver contains all entries in the other hstore:
89
+ #
90
+ # hstore_op.contains(:h) # (hstore @> h)
91
+ def contains(other)
92
+ bool_op(CONTAINS, other)
93
+ end
94
+
95
+ # Check if the other hstore contains all entries in the receiver:
96
+ #
97
+ # hstore_op.contained_by(:h) # (hstore <@ h)
98
+ def contained_by(other)
99
+ bool_op(CONTAINED_BY, other)
100
+ end
101
+
102
+ # Check if the receiver contains a non-NULL value for the given key:
103
+ #
104
+ # hstore_op.defined('a') # defined(hstore, 'a')
105
+ def defined(key)
106
+ Sequel::SQL::BooleanExpression.new(:NOOP, function(:defined, key))
107
+ end
108
+
109
+ # Delete the matching entries from the receiver:
110
+ #
111
+ # hstore_op.delete('a') # delete(hstore, 'a')
112
+ def delete(key)
113
+ HStoreOp.new(function(:delete, key))
114
+ end
115
+
116
+ # Transform the receiver into a set of keys and values:
117
+ #
118
+ # hstore_op.each # each(hstore)
119
+ def each
120
+ function(:each)
121
+ end
122
+
123
+ # Check if the receiver contains the given key:
124
+ #
125
+ # hstore_op.has_key?('a') # (hstore ? 'a')
126
+ def has_key?(key)
127
+ bool_op(HAS_KEY, key)
128
+ end
129
+ alias include? has_key?
130
+ alias key? has_key?
131
+ alias member? has_key?
132
+ alias exist? has_key?
133
+
134
+ # Return the receiver.
135
+ def hstore
136
+ self
137
+ end
138
+
139
+ # Return the keys as a PostgreSQL array:
140
+ #
141
+ # hstore_op.keys # akeys(hstore)
142
+ def keys
143
+ function(:akeys)
144
+ end
145
+ alias akeys keys
146
+
147
+ # Merge a given hstore into the receiver:
148
+ #
149
+ # hstore_op.merge(:a) # (hstore || a)
150
+ def merge(other)
151
+ HStoreOp.new(Sequel::SQL::PlaceholderLiteralString.new(CONCAT, [self, other]))
152
+ end
153
+ alias concat merge
154
+
155
+ # Create a new record populated with entries from the receiver:
156
+ #
157
+ # hstore_op.populate(:a) # populate_record(a, hstore)
158
+ def populate(record)
159
+ SQL::Function.new(:populate_record, record, self)
160
+ end
161
+
162
+ # Update the values in a record using entries in the receiver:
163
+ #
164
+ # hstore_op.record_set(:a) # (a #= hstore)
165
+ def record_set(record)
166
+ Sequel::SQL::PlaceholderLiteralString.new(RECORD_SET, [record, value])
167
+ end
168
+
169
+ # Return the keys as a PostgreSQL set:
170
+ #
171
+ # hstore_op.skeys # skeys(hstore)
172
+ def skeys
173
+ function(:skeys)
174
+ end
175
+
176
+ # Return an hstore with only the keys in the given array:
177
+ #
178
+ # hstore_op.slice(:a) # slice(hstore, a)
179
+ def slice(keys)
180
+ HStoreOp.new(function(:slice, keys))
181
+ end
182
+
183
+ # Return the values as a PostgreSQL set:
184
+ #
185
+ # hstore_op.svals # svals(hstore)
186
+ def svals
187
+ function(:svals)
188
+ end
189
+
190
+ # Return a flattened array of the receiver with alternating
191
+ # keys and values:
192
+ #
193
+ # hstore_op.to_array # hstore_to_array(hstore)
194
+ def to_array
195
+ function(:hstore_to_array)
196
+ end
197
+
198
+ # Return a nested array of the receiver, with arrays of
199
+ # 2 element (key/value) arrays:
200
+ #
201
+ # hstore_op.to_matrix # hstore_to_matrix(hstore)
202
+ def to_matrix
203
+ function(:hstore_to_matrix)
204
+ end
205
+
206
+ # Return the values as a PostgreSQL array:
207
+ #
208
+ # hstore_op.values # avals(hstore)
209
+ def values
210
+ function(:avals)
211
+ end
212
+ alias avals values
213
+
214
+ private
215
+
216
+ # Return a placeholder literal with the given str and args, wrapped
217
+ # in a boolean expression, used by operators that return booleans.
218
+ def bool_op(str, other)
219
+ Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(str, [value, other]))
220
+ end
221
+
222
+ # Return a function with the given name, and the receiver as the first
223
+ # argument, with any additional arguments given.
224
+ def function(name, *args)
225
+ SQL::Function.new(name, self, *args)
226
+ end
227
+ end
228
+
229
+ module HStoreOpMethods
230
+ # Wrap the receiver in an HStoreOp so you can easily use the PostgreSQL
231
+ # hstore functions and operators with it.
232
+ def hstore
233
+ HStoreOp.new(self)
234
+ end
235
+ end
236
+
237
+ if defined?(HStore)
238
+ class HStore
239
+ # Wrap the receiver in an HStoreOp so you can easily use the PostgreSQL
240
+ # hstore functions and operators with it.
241
+ def op
242
+ HStoreOp.new(self)
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ class SQL::GenericExpression
249
+ include Sequel::Postgres::HStoreOpMethods
250
+ end
251
+
252
+ class LiteralString
253
+ include Sequel::Postgres::HStoreOpMethods
254
+ end
255
+ end
256
+
257
+ class Symbol
258
+ include Sequel::Postgres::HStoreOpMethods
259
+ end
@@ -0,0 +1,316 @@
1
+ # This extension adds a statement cache to Sequel's postgres adapter,
2
+ # with the ability to automatically prepare statements that are
3
+ # executed repeatedly. When combined with the pg_auto_parameterize
4
+ # extension, it can take Sequel code such as:
5
+ #
6
+ # DB.extend Sequel::Postgres::AutoParameterize::DatabaseMethods
7
+ # DB.extend Sequel::Postgres::StatementCache::DatabaseMethods
8
+ # DB[:table].filter(:a=>1)
9
+ # DB[:table].filter(:a=>2)
10
+ # DB[:table].filter(:a=>3)
11
+ #
12
+ # And use the same prepared statement to execute the queries.
13
+ #
14
+ # The backbone of this extension is a modified LRU cache. It considers
15
+ # both the last executed time and the number of executions when
16
+ # determining which queries to keep in the cache. It only cleans the
17
+ # cache when a high water mark has been passed, and removes queries
18
+ # until it reaches the low water mark, in order to avoid thrashing when
19
+ # you are using more than the maximum number of queries. To avoid
20
+ # preparing queries when it isn't necessary, it does not prepare them
21
+ # on the server side unless they are being executed more than once.
22
+ # The cache is very tunable, allowing you to set the high and low
23
+ # water marks, the number of executions before preparing the query,
24
+ # and even use a custom callback for determine which queries to keep
25
+ # in the cache.
26
+ #
27
+ # Note that automatically preparing statements does have some issues.
28
+ # Most notably, if you change the result type that the query returns,
29
+ # PostgreSQL will raise an error. This can happen if you have
30
+ # prepared a statement that selects all columns from a table, and then
31
+ # you add or remove a column from that table. This extension does
32
+ # attempt to check that case and clear the statement caches if you use
33
+ # alter_table from within Sequel, but it cannot fix the case when such
34
+ # a change is made externally.
35
+ #
36
+ # This extension only works when the pg driver is used as the backend
37
+ # for the postgres adapter.
38
+
39
+ module Sequel
40
+ module Postgres
41
+ module StatementCache
42
+ # A simple structure used for the values in the StatementCache's hash.
43
+ # It does not hold the related SQL, since that is used as the key for
44
+ # the StatementCache's hash.
45
+ class Statement
46
+ # The last time this statement was seen by the cache, persumably the
47
+ # last time it was executed.
48
+ attr_accessor :last_seen
49
+
50
+ # The total number of executions since the statement entered the cache.
51
+ attr_accessor :num_executes
52
+
53
+ # The id related to the statement, used as part of the prepared statement
54
+ # name.
55
+ attr_reader :cache_id
56
+
57
+ # Used when adding entries to the cache, just sets their id. Uses
58
+ # 0 for num_executes since that is incremented elsewhere. Does not
59
+ # set last_seen since that is set elsewhere to reduce branching.
60
+ def initialize(cache_id)
61
+ @num_executes = 0
62
+ @cache_id = cache_id
63
+ end
64
+
65
+ # The name to use for the server side prepared statement. Note that this
66
+ # statement might not actually be prepared.
67
+ def name
68
+ "sequel_pgap_#{cache_id}"
69
+ end
70
+ end
71
+
72
+ # The backbone of the block, a modified LRU (least recently used) cache
73
+ # mapping SQL query strings to Statement objects.
74
+ class StatementCache
75
+ include Enumerable
76
+
77
+ # Set the options for the statement cache. These are generally set at
78
+ # the database level using the :statement_cache_opts Database option.
79
+ #
80
+ # :max_size :: The maximum size (high water mark) for the cache. If
81
+ # an entry is added when the current size of the cache is
82
+ # equal to the maximum size, the cache is cleaned up to
83
+ # reduce the number of entries to the :min_size. Defaults
84
+ # to 1000.
85
+ # :min_size :: The minimum size (low water mark) for the cache. On
86
+ # cleanup, the size of the cache is reduced to this
87
+ # number. Note that there could be fewer than this
88
+ # number of entries in the cache. Defaults to :max_size/2.
89
+ # :prepare_after :: The number of executions to wait for before preparing
90
+ # the query server-side. If set to 1, prepares all executed
91
+ # queries server-side. If set to 5, does not attempt to
92
+ # prepare the query until the 5th execution. Defaults to 2.
93
+ # :sorter :: A callable object that takes two arguments, the current time
94
+ # and the related Statement instance, and should return some
95
+ # Comparable (usually a numeric) such that the lowest values
96
+ # returned are the first to be removed when it comes time to
97
+ # clean the pool. The default is basically:
98
+ #
99
+ # lambda{|t, stmt| (stmt.last_seen - t)/stmt.num_executes}
100
+ #
101
+ # so that it doesn't remove statements that have been executed
102
+ # many times just because many less-frequently executed statements
103
+ # have been executed recently.
104
+ #
105
+ # The block passed is called with the Statement object's name, only for
106
+ # statements that have been prepared, and should be used to deallocate the
107
+ # statements.
108
+ def initialize(opts={}, &block)
109
+ @cleanup_proc = block
110
+ @prepare_after = opts.fetch(:prepare_after, 2)
111
+ @max_size = opts.fetch(:max_size, 1000)
112
+ @min_size = opts.fetch(:min_size, @max_size/2)
113
+ @sorter = opts.fetch(:sorter){method(:default_sorter)}
114
+ @ids = (1..@max_size).to_a.reverse
115
+ @hash = {}
116
+ #
117
+ # We add one so that when we clean the cache, the entry
118
+ # about to be added brings us to the min_size.
119
+ @size_diff = @max_size - @min_size + 1
120
+ end
121
+
122
+ # Completely clear the statement cache, deallocating on
123
+ # the server side all statements that have been prepared.
124
+ def clear
125
+ @hash.keys.each{|k| remove(k)}
126
+ end
127
+
128
+ # Yield each SQL string and Statement instance in the cache
129
+ # to the block.
130
+ def each(&block)
131
+ @hash.each(&block)
132
+ end
133
+
134
+ # Get the related statement name from the cache. If the
135
+ # entry is already in the cache, just bump it's last seen
136
+ # time and the number of executions. Otherwise, add it
137
+ # to the cache. If the cache is already full, clean it up
138
+ # before adding it.
139
+ #
140
+ # If the num of executions has passed the threshhold, yield
141
+ # the statement name to the block, which should be used to
142
+ # prepare the statement on the server side.
143
+ #
144
+ # This method should return the prepared statment name if
145
+ # the statement has been prepared, and nil if the query
146
+ # has not been prepared and the statement should be executed
147
+ # normally.
148
+ def fetch(sql)
149
+ unless stmt = @hash[sql]
150
+ # Get the next id from the id pool.
151
+ unless id = @ids.pop
152
+ # No id left, cache must be full, so cleanup and then
153
+ # get the next id from the id pool.
154
+ cleanup
155
+ id = @ids.pop
156
+ end
157
+ @hash[sql] = stmt = Statement.new(id)
158
+ end
159
+
160
+ stmt.last_seen = Time.now
161
+ stmt.num_executes += 1
162
+
163
+ if stmt.num_executes >= @prepare_after
164
+ if stmt.num_executes == @prepare_after
165
+ begin
166
+ yield(stmt.name)
167
+ rescue PGError
168
+ # An error occurred while preparing the statement,
169
+ # execute it normally (which will probably raise
170
+ # the error again elsewhere), but decrement the
171
+ # number of executions so we don't think we've
172
+ # prepared the statement when we haven't.
173
+ stmt.num_executes -= 1
174
+ return nil
175
+ end
176
+ end
177
+ stmt.name
178
+ end
179
+ end
180
+
181
+ # The current size of the statement cache.
182
+ def size
183
+ @hash.length
184
+ end
185
+
186
+ private
187
+
188
+ # Sort by time since last execution and number of executions.
189
+ # We don't want to throw stuff out of the
190
+ # cache if it has been executed a lot,
191
+ # but a bunch of queries that were
192
+ # executed only once came in more recently.
193
+ def default_sorter(t, stmt)
194
+ (stmt.last_seen - t)/stmt.num_executes
195
+ end
196
+
197
+ # After sorting the cache appropriately (so that the least important
198
+ # items are first), reduce the number of entries in the cache to
199
+ # the low water mark by removing the related query. Should only be
200
+ # called when the cache is full.
201
+ def cleanup
202
+ t = Time.now
203
+ @hash.sort_by{|k,v| @sorter.call(t, v)}.first(@size_diff).each{|sql, stmt| remove(sql)}
204
+ end
205
+
206
+ # Remove the query from the cache. If it has been prepared,
207
+ # call the cleanup_proc to deallocate the statement.
208
+ def remove(sql)
209
+ stmt = @hash.delete(sql)
210
+ if stmt.num_executes >= @prepare_after
211
+ @cleanup_proc.call(stmt.name)
212
+ end
213
+
214
+ # Return id to the pool of ids
215
+ @ids.push(stmt.cache_id)
216
+ end
217
+ end
218
+
219
+ module AdapterMethods
220
+ # A regular expression for the types of queries to cache. Any queries not
221
+ # matching this regular expression are not cached.
222
+ DML_RE = /\A(WITH|SELECT|INSERT|UPDATE|DELETE) /
223
+
224
+ # The StatementCache instance for this connection. Note that
225
+ # each connection has a separate StatementCache, because prepared
226
+ # statements are connection-specific.
227
+ attr_reader :statement_cache
228
+
229
+ # Set the statement_cache for the connection, using the database's
230
+ # :statement_cache_opts option.
231
+ def self.extended(c)
232
+ c.instance_variable_set(:@statement_cache, StatementCache.new(c.sequel_db.opts[:statement_cache_opts] || {}){|name| c.deallocate(name)})
233
+ end
234
+
235
+ # pg seems to already use the db method (but not the @db instance variable),
236
+ # so use the sequel_db method to access the related Sequel::Database object.
237
+ def sequel_db
238
+ @db
239
+ end
240
+
241
+ # Deallocate on the server the prepared statement with the given name.
242
+ def deallocate(name)
243
+ begin
244
+ execute("DEALLOCATE #{name}")
245
+ rescue PGError
246
+ # table probably got removed, just ignore it
247
+ end
248
+ end
249
+
250
+ private
251
+
252
+ # If the sql query string is one we should cache, cache it. If the query already
253
+ # has a related prepared statement with it, execute the prepared statement instead
254
+ # of executing the query normally.
255
+ def execute_query(sql, args=nil)
256
+ if sql =~ DML_RE
257
+ if name = statement_cache.fetch(sql){|stmt_name| sequel_db.log_yield("PREPARE #{stmt_name} AS #{sql}"){prepare(stmt_name, sql)}}
258
+ if args
259
+ sequel_db.log_yield("EXECUTE #{name} (#{sql})", args){exec_prepared(name, args)}
260
+ else
261
+ sequel_db.log_yield("EXECUTE #{name} (#{sql})"){exec_prepared(name)}
262
+ end
263
+ else
264
+ super
265
+ end
266
+ else
267
+ super
268
+ end
269
+ end
270
+ end
271
+
272
+ module DatabaseMethods
273
+ # Setup the after_connect proc for the connection pool to make
274
+ # sure the connection object is extended with the appropriate
275
+ # module. This disconnects any existing connections to ensure
276
+ # that all connections in the pool have been extended appropriately.
277
+ def self.extended(db)
278
+ # Respect existing after_connect proc if one is present
279
+ pr = db.opts[:after_connect]
280
+
281
+ # Set the after_connect proc to extend the adapter with
282
+ # the statement cache support.
283
+ db.pool.after_connect = db.opts[:after_connect] = proc do |c|
284
+ pr.call(c) if pr
285
+ c.extend(AdapterMethods)
286
+ end
287
+
288
+ # Disconnect to make sure all connections get set up with
289
+ # statement cache.
290
+ db.disconnect
291
+ end
292
+
293
+ # Clear statement caches for all connections when altering tables.
294
+ def alter_table(*)
295
+ clear_statement_caches
296
+ super
297
+ end
298
+
299
+ # Clear statement caches for all connections when dropping tables.
300
+ def drop_table(*)
301
+ clear_statement_caches
302
+ super
303
+ end
304
+
305
+ private
306
+
307
+ # Clear the statement cache for all connections. Note that for
308
+ # the threaded pools, this will not affect connections currently
309
+ # allocated to other threads.
310
+ def clear_statement_caches
311
+ pool.all_connections{|c| c.statement_cache.clear}
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end