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.
- 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
|