sequel 3.33.0 → 3.34.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +140 -0
- data/Rakefile +7 -0
- data/bin/sequel +22 -2
- data/doc/dataset_basics.rdoc +1 -1
- data/doc/mass_assignment.rdoc +3 -1
- data/doc/querying.rdoc +28 -4
- data/doc/reflection.rdoc +23 -3
- data/doc/release_notes/3.34.0.txt +671 -0
- data/doc/schema_modification.rdoc +18 -2
- data/doc/virtual_rows.rdoc +49 -0
- data/lib/sequel/adapters/do/mysql.rb +0 -5
- data/lib/sequel/adapters/ibmdb.rb +9 -4
- data/lib/sequel/adapters/jdbc.rb +9 -4
- data/lib/sequel/adapters/jdbc/h2.rb +8 -2
- data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
- data/lib/sequel/adapters/jdbc/postgresql.rb +43 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +19 -0
- data/lib/sequel/adapters/mock.rb +24 -3
- data/lib/sequel/adapters/mysql.rb +29 -50
- data/lib/sequel/adapters/mysql2.rb +13 -28
- data/lib/sequel/adapters/oracle.rb +8 -2
- data/lib/sequel/adapters/postgres.rb +115 -20
- data/lib/sequel/adapters/shared/db2.rb +1 -1
- data/lib/sequel/adapters/shared/mssql.rb +14 -3
- data/lib/sequel/adapters/shared/mysql.rb +59 -11
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
- data/lib/sequel/adapters/shared/oracle.rb +1 -1
- data/lib/sequel/adapters/shared/postgres.rb +127 -30
- data/lib/sequel/adapters/shared/sqlite.rb +55 -38
- data/lib/sequel/adapters/sqlite.rb +9 -3
- data/lib/sequel/adapters/swift.rb +2 -2
- data/lib/sequel/adapters/swift/mysql.rb +0 -5
- data/lib/sequel/adapters/swift/postgres.rb +10 -0
- data/lib/sequel/ast_transformer.rb +4 -0
- data/lib/sequel/connection_pool.rb +8 -0
- data/lib/sequel/connection_pool/sharded_single.rb +5 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +17 -0
- data/lib/sequel/connection_pool/single.rb +5 -0
- data/lib/sequel/connection_pool/threaded.rb +14 -0
- data/lib/sequel/core.rb +24 -3
- data/lib/sequel/database/connecting.rb +24 -14
- data/lib/sequel/database/dataset_defaults.rb +1 -0
- data/lib/sequel/database/misc.rb +16 -25
- data/lib/sequel/database/query.rb +20 -2
- data/lib/sequel/database/schema_generator.rb +2 -2
- data/lib/sequel/database/schema_methods.rb +120 -23
- data/lib/sequel/dataset/actions.rb +91 -18
- data/lib/sequel/dataset/features.rb +5 -0
- data/lib/sequel/dataset/prepared_statements.rb +6 -2
- data/lib/sequel/dataset/sql.rb +68 -51
- data/lib/sequel/extensions/_pretty_table.rb +79 -0
- data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +18 -13
- data/lib/sequel/extensions/migration.rb +4 -0
- data/lib/sequel/extensions/null_dataset.rb +90 -0
- data/lib/sequel/extensions/pg_array.rb +460 -0
- data/lib/sequel/extensions/pg_array_ops.rb +220 -0
- data/lib/sequel/extensions/pg_auto_parameterize.rb +174 -0
- data/lib/sequel/extensions/pg_hstore.rb +296 -0
- data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
- data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
- data/lib/sequel/extensions/pretty_table.rb +5 -71
- data/lib/sequel/extensions/query_literals.rb +79 -0
- data/lib/sequel/extensions/schema_caching.rb +76 -0
- data/lib/sequel/extensions/schema_dumper.rb +227 -31
- data/lib/sequel/extensions/select_remove.rb +35 -0
- data/lib/sequel/extensions/sql_expr.rb +4 -110
- data/lib/sequel/extensions/to_dot.rb +1 -1
- data/lib/sequel/model.rb +11 -2
- data/lib/sequel/model/associations.rb +35 -7
- data/lib/sequel/model/base.rb +159 -36
- data/lib/sequel/no_core_ext.rb +2 -0
- data/lib/sequel/plugins/caching.rb +25 -18
- data/lib/sequel/plugins/composition.rb +1 -1
- data/lib/sequel/plugins/hook_class_methods.rb +1 -1
- data/lib/sequel/plugins/identity_map.rb +11 -3
- data/lib/sequel/plugins/instance_filters.rb +10 -0
- data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
- data/lib/sequel/plugins/nested_attributes.rb +4 -3
- data/lib/sequel/plugins/prepared_statements.rb +3 -1
- data/lib/sequel/plugins/prepared_statements_associations.rb +5 -1
- data/lib/sequel/plugins/schema.rb +7 -2
- data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
- data/lib/sequel/plugins/static_cache.rb +99 -0
- data/lib/sequel/plugins/validation_class_methods.rb +1 -1
- data/lib/sequel/sql.rb +417 -7
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/firebird_spec.rb +1 -1
- data/spec/adapters/mssql_spec.rb +12 -15
- data/spec/adapters/mysql_spec.rb +81 -23
- data/spec/adapters/postgres_spec.rb +444 -77
- data/spec/adapters/spec_helper.rb +2 -0
- data/spec/adapters/sqlite_spec.rb +8 -8
- data/spec/core/connection_pool_spec.rb +85 -0
- data/spec/core/database_spec.rb +29 -5
- data/spec/core/dataset_spec.rb +171 -3
- data/spec/core/expression_filters_spec.rb +364 -0
- data/spec/core/mock_adapter_spec.rb +17 -3
- data/spec/core/schema_spec.rb +133 -0
- data/spec/extensions/association_dependencies_spec.rb +13 -13
- data/spec/extensions/caching_spec.rb +26 -3
- data/spec/extensions/class_table_inheritance_spec.rb +2 -2
- data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +23 -94
- data/spec/extensions/force_encoding_spec.rb +4 -2
- data/spec/extensions/hook_class_methods_spec.rb +5 -2
- data/spec/extensions/identity_map_spec.rb +17 -0
- data/spec/extensions/instance_filters_spec.rb +1 -1
- data/spec/extensions/lazy_attributes_spec.rb +2 -2
- data/spec/extensions/list_spec.rb +4 -4
- data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
- data/spec/extensions/migration_spec.rb +6 -2
- data/spec/extensions/nested_attributes_spec.rb +20 -0
- data/spec/extensions/null_dataset_spec.rb +85 -0
- data/spec/extensions/optimistic_locking_spec.rb +2 -2
- data/spec/extensions/pg_array_ops_spec.rb +105 -0
- data/spec/extensions/pg_array_spec.rb +196 -0
- data/spec/extensions/pg_auto_parameterize_spec.rb +64 -0
- data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
- data/spec/extensions/pg_hstore_spec.rb +195 -0
- data/spec/extensions/pg_statement_cache_spec.rb +209 -0
- data/spec/extensions/prepared_statements_spec.rb +4 -0
- data/spec/extensions/pretty_table_spec.rb +6 -0
- data/spec/extensions/query_literals_spec.rb +168 -0
- data/spec/extensions/schema_caching_spec.rb +41 -0
- data/spec/extensions/schema_dumper_spec.rb +231 -11
- data/spec/extensions/schema_spec.rb +14 -2
- data/spec/extensions/select_remove_spec.rb +38 -0
- data/spec/extensions/sharding_spec.rb +6 -6
- data/spec/extensions/skip_create_refresh_spec.rb +1 -1
- data/spec/extensions/spec_helper.rb +2 -1
- data/spec/extensions/sql_expr_spec.rb +28 -19
- data/spec/extensions/static_cache_spec.rb +145 -0
- data/spec/extensions/touch_spec.rb +1 -1
- data/spec/extensions/typecast_on_load_spec.rb +9 -1
- data/spec/integration/associations_test.rb +6 -6
- data/spec/integration/database_test.rb +1 -1
- data/spec/integration/dataset_test.rb +89 -26
- data/spec/integration/migrator_test.rb +2 -3
- data/spec/integration/model_test.rb +3 -3
- data/spec/integration/plugin_test.rb +85 -22
- data/spec/integration/prepared_statement_test.rb +28 -8
- data/spec/integration/schema_test.rb +78 -7
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/integration/timezone_test.rb +1 -1
- data/spec/integration/transaction_test.rb +4 -6
- data/spec/integration/type_test.rb +2 -2
- data/spec/model/associations_spec.rb +94 -8
- data/spec/model/base_spec.rb +4 -4
- data/spec/model/hooks_spec.rb +2 -2
- data/spec/model/model_spec.rb +19 -7
- data/spec/model/record_spec.rb +135 -58
- data/spec/model/spec_helper.rb +1 -0
- 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
|