sequel 4.45.0 → 4.46.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +108 -0
- data/doc/release_notes/4.46.0.txt +404 -0
- data/doc/security.rdoc +9 -0
- data/doc/sql.rdoc +2 -2
- data/doc/testing.rdoc +1 -1
- data/doc/validations.rdoc +1 -2
- data/lib/sequel/adapters/ado.rb +8 -3
- data/lib/sequel/adapters/ado/access.rb +8 -4
- data/lib/sequel/adapters/ado/mssql.rb +3 -1
- data/lib/sequel/adapters/amalgalite.rb +5 -0
- data/lib/sequel/adapters/cubrid.rb +16 -7
- data/lib/sequel/adapters/do.rb +7 -1
- data/lib/sequel/adapters/do/mysql.rb +8 -4
- data/lib/sequel/adapters/ibmdb.rb +10 -5
- data/lib/sequel/adapters/jdbc.rb +8 -2
- data/lib/sequel/adapters/jdbc/as400.rb +10 -3
- data/lib/sequel/adapters/jdbc/db2.rb +27 -16
- data/lib/sequel/adapters/jdbc/derby.rb +47 -20
- data/lib/sequel/adapters/jdbc/h2.rb +13 -7
- data/lib/sequel/adapters/jdbc/hsqldb.rb +18 -9
- data/lib/sequel/adapters/jdbc/mssql.rb +5 -2
- data/lib/sequel/adapters/jdbc/mysql.rb +3 -2
- data/lib/sequel/adapters/jdbc/oracle.rb +3 -2
- data/lib/sequel/adapters/jdbc/postgresql.rb +4 -3
- data/lib/sequel/adapters/jdbc/sqlanywhere.rb +2 -1
- data/lib/sequel/adapters/jdbc/sqlite.rb +10 -3
- data/lib/sequel/adapters/jdbc/sqlserver.rb +23 -0
- data/lib/sequel/adapters/jdbc/transactions.rb +16 -10
- data/lib/sequel/adapters/mock.rb +5 -0
- data/lib/sequel/adapters/mysql.rb +8 -1
- data/lib/sequel/adapters/mysql2.rb +6 -1
- data/lib/sequel/adapters/odbc.rb +20 -8
- data/lib/sequel/adapters/odbc/mssql.rb +6 -3
- data/lib/sequel/adapters/oracle.rb +12 -6
- data/lib/sequel/adapters/postgres.rb +20 -8
- data/lib/sequel/adapters/shared/access.rb +76 -47
- data/lib/sequel/adapters/shared/cubrid.rb +16 -11
- data/lib/sequel/adapters/shared/db2.rb +46 -19
- data/lib/sequel/adapters/shared/firebird.rb +20 -8
- data/lib/sequel/adapters/shared/informix.rb +6 -3
- data/lib/sequel/adapters/shared/mssql.rb +132 -72
- data/lib/sequel/adapters/shared/mysql.rb +112 -65
- data/lib/sequel/adapters/shared/oracle.rb +36 -21
- data/lib/sequel/adapters/shared/postgres.rb +91 -56
- data/lib/sequel/adapters/shared/sqlanywhere.rb +65 -37
- data/lib/sequel/adapters/shared/sqlite.rb +67 -32
- data/lib/sequel/adapters/sqlanywhere.rb +9 -1
- data/lib/sequel/adapters/sqlite.rb +8 -1
- data/lib/sequel/adapters/swift.rb +5 -0
- data/lib/sequel/adapters/swift/mysql.rb +4 -2
- data/lib/sequel/adapters/swift/sqlite.rb +1 -1
- data/lib/sequel/adapters/tinytds.rb +10 -3
- data/lib/sequel/adapters/utils/emulate_offset_with_reverse_and_count.rb +1 -1
- data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +1 -1
- data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -0
- data/lib/sequel/adapters/utils/pg_types.rb +14 -6
- data/lib/sequel/adapters/utils/replace.rb +4 -2
- data/lib/sequel/connection_pool/single.rb +2 -2
- data/lib/sequel/core.rb +24 -11
- data/lib/sequel/database/connecting.rb +9 -3
- data/lib/sequel/database/dataset_defaults.rb +7 -1
- data/lib/sequel/database/logging.rb +1 -0
- data/lib/sequel/database/misc.rb +5 -2
- data/lib/sequel/database/query.rb +7 -5
- data/lib/sequel/database/schema_generator.rb +1 -0
- data/lib/sequel/database/schema_methods.rb +50 -27
- data/lib/sequel/database/transactions.rb +19 -9
- data/lib/sequel/dataset/actions.rb +15 -6
- data/lib/sequel/dataset/graph.rb +15 -5
- data/lib/sequel/dataset/misc.rb +12 -4
- data/lib/sequel/dataset/mutation.rb +17 -8
- data/lib/sequel/dataset/prepared_statements.rb +3 -2
- data/lib/sequel/dataset/query.rb +84 -38
- data/lib/sequel/dataset/sql.rb +302 -191
- data/lib/sequel/deprecated.rb +26 -17
- data/lib/sequel/extensions/_deprecated_identifier_mangling.rb +2 -2
- data/lib/sequel/extensions/auto_literal_strings.rb +74 -0
- data/lib/sequel/extensions/from_block.rb +1 -0
- data/lib/sequel/extensions/graph_each.rb +1 -1
- data/lib/sequel/extensions/identifier_mangling.rb +2 -2
- data/lib/sequel/extensions/migration.rb +28 -4
- data/lib/sequel/extensions/no_auto_literal_strings.rb +2 -0
- data/lib/sequel/extensions/schema_dumper.rb +4 -4
- data/lib/sequel/extensions/sequel_3_dataset_methods.rb +5 -3
- data/lib/sequel/extensions/set_overrides.rb +2 -0
- data/lib/sequel/extensions/split_array_nil.rb +2 -2
- data/lib/sequel/extensions/virtual_row_method_block.rb +44 -0
- data/lib/sequel/model.rb +11 -7
- data/lib/sequel/model/associations.rb +5 -7
- data/lib/sequel/model/base.rb +47 -45
- data/lib/sequel/model/dataset_module.rb +9 -14
- data/lib/sequel/model/plugins.rb +3 -0
- data/lib/sequel/no_core_ext.rb +1 -0
- data/lib/sequel/plugins/blacklist_security.rb +1 -1
- data/lib/sequel/plugins/boolean_subsets.rb +7 -5
- data/lib/sequel/plugins/class_table_inheritance.rb +47 -10
- data/lib/sequel/plugins/dataset_associations.rb +1 -1
- data/lib/sequel/plugins/def_dataset_method.rb +90 -0
- data/lib/sequel/plugins/finder.rb +240 -0
- data/lib/sequel/plugins/inverted_subsets.rb +19 -12
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +1 -1
- data/lib/sequel/plugins/schema.rb +1 -1
- data/lib/sequel/plugins/single_table_inheritance.rb +7 -1
- data/lib/sequel/plugins/subset_conditions.rb +11 -3
- data/lib/sequel/plugins/whitelist_security.rb +118 -0
- data/lib/sequel/sql.rb +80 -36
- data/lib/sequel/timezones.rb +2 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +20 -0
- data/spec/adapters/mysql_spec.rb +1 -1
- data/spec/adapters/oracle_spec.rb +12 -8
- data/spec/adapters/postgres_spec.rb +1 -1
- data/spec/adapters/spec_helper.rb +1 -1
- data/spec/adapters/sqlite_spec.rb +36 -34
- data/spec/core/connection_pool_spec.rb +2 -1
- data/spec/core/database_spec.rb +87 -9
- data/spec/core/dataset_spec.rb +501 -129
- data/spec/core/deprecated_spec.rb +1 -1
- data/spec/core/expression_filters_spec.rb +146 -60
- data/spec/core/mock_adapter_spec.rb +1 -1
- data/spec/core/object_graph_spec.rb +61 -9
- data/spec/core/placeholder_literalizer_spec.rb +20 -2
- data/spec/core/schema_generator_spec.rb +6 -6
- data/spec/core/schema_spec.rb +54 -5
- data/spec/core_extensions_spec.rb +122 -18
- data/spec/deprecation_helper.rb +27 -2
- data/spec/extensions/_deprecated_identifier_mangling_spec.rb +6 -6
- data/spec/extensions/association_proxies_spec.rb +2 -2
- data/spec/extensions/auto_literal_strings_spec.rb +212 -0
- data/spec/extensions/blacklist_security_spec.rb +1 -0
- data/spec/extensions/class_table_inheritance_spec.rb +1037 -39
- data/spec/extensions/column_select_spec.rb +20 -8
- data/spec/extensions/columns_introspection_spec.rb +3 -3
- data/spec/extensions/core_refinements_spec.rb +29 -12
- data/spec/extensions/dataset_associations_spec.rb +12 -12
- data/spec/extensions/def_dataset_method_spec.rb +100 -0
- data/spec/extensions/error_sql_spec.rb +1 -1
- data/spec/extensions/finder_spec.rb +260 -0
- data/spec/extensions/graph_each_spec.rb +2 -2
- data/spec/extensions/identifier_mangling_spec.rb +14 -8
- data/spec/extensions/inverted_subsets_spec.rb +4 -4
- data/spec/extensions/lazy_attributes_spec.rb +7 -0
- data/spec/extensions/many_through_many_spec.rb +38 -14
- data/spec/extensions/nested_attributes_spec.rb +18 -6
- data/spec/extensions/no_auto_literal_strings_spec.rb +1 -1
- data/spec/extensions/pg_enum_spec.rb +16 -1
- data/spec/extensions/pg_interval_spec.rb +11 -2
- data/spec/extensions/pg_loose_count_spec.rb +5 -0
- data/spec/extensions/pg_row_spec.rb +25 -0
- data/spec/extensions/prepared_statements_spec.rb +10 -1
- data/spec/extensions/query_spec.rb +2 -2
- data/spec/extensions/schema_dumper_spec.rb +2 -2
- data/spec/extensions/schema_spec.rb +2 -2
- data/spec/extensions/set_overrides_spec.rb +7 -3
- data/spec/extensions/sql_expr_spec.rb +0 -1
- data/spec/extensions/subset_conditions_spec.rb +6 -6
- data/spec/extensions/table_select_spec.rb +24 -12
- data/spec/extensions/to_dot_spec.rb +4 -4
- data/spec/extensions/whitelist_security_spec.rb +131 -0
- data/spec/integration/dataset_test.rb +9 -5
- data/spec/integration/model_test.rb +2 -0
- data/spec/integration/plugin_test.rb +2 -2
- data/spec/integration/spec_helper.rb +1 -1
- data/spec/model/associations_spec.rb +39 -11
- data/spec/model/base_spec.rb +44 -24
- data/spec/model/class_dataset_methods_spec.rb +18 -16
- data/spec/model/dataset_methods_spec.rb +4 -4
- data/spec/model/eager_loading_spec.rb +84 -24
- data/spec/model/model_spec.rb +97 -63
- data/spec/model/record_spec.rb +21 -13
- metadata +13 -2
@@ -119,6 +119,10 @@ module Sequel
|
|
119
119
|
#
|
120
120
|
# # Some examples of using these options:
|
121
121
|
#
|
122
|
+
# # Use a subquery for all subclass datasets, fixing issues with ambiguous
|
123
|
+
# # column names.
|
124
|
+
# Employee.plugin :class_table_inheritance, :key=>:kind, :alias=>:employees
|
125
|
+
#
|
122
126
|
# # Specifying the tables with a :table_map hash
|
123
127
|
# Employee.plugin :class_table_inheritance,
|
124
128
|
# :table_map=>{:Employee => :employees,
|
@@ -179,6 +183,8 @@ module Sequel
|
|
179
183
|
end
|
180
184
|
|
181
185
|
# Initialize the plugin using the following options:
|
186
|
+
# :alias :: Use a subquery for each subclass dataset that joins to another table,
|
187
|
+
# using this as the alias.
|
182
188
|
# :key :: Column symbol that holds the key that identifies the class to use.
|
183
189
|
# Necessary if you want to call model methods on a superclass
|
184
190
|
# that return subclass instances
|
@@ -197,6 +203,7 @@ module Sequel
|
|
197
203
|
@cti_instance_dataset = @instance_dataset
|
198
204
|
@cti_table_columns = columns
|
199
205
|
@cti_table_map = opts[:table_map] || {}
|
206
|
+
@cti_alias = opts[:alias]
|
200
207
|
end
|
201
208
|
end
|
202
209
|
|
@@ -235,7 +242,7 @@ module Sequel
|
|
235
242
|
# For backwards compatibility.
|
236
243
|
def cti_columns
|
237
244
|
h = {}
|
238
|
-
cti_models.each { |m| h[m.
|
245
|
+
cti_models.each { |m| h[m.cti_table_name] = m.cti_table_columns }
|
239
246
|
h
|
240
247
|
end
|
241
248
|
|
@@ -256,7 +263,7 @@ module Sequel
|
|
256
263
|
super
|
257
264
|
end
|
258
265
|
|
259
|
-
Plugins.inherited_instance_variables(self, :@cti_models=>nil, :@cti_tables=>nil, :@cti_table_columns=>nil, :@cti_instance_dataset=>nil, :@cti_table_map=>nil)
|
266
|
+
Plugins.inherited_instance_variables(self, :@cti_models=>nil, :@cti_tables=>nil, :@cti_table_columns=>nil, :@cti_instance_dataset=>nil, :@cti_table_map=>nil, :@cti_alias=>nil)
|
260
267
|
|
261
268
|
def inherited(subclass)
|
262
269
|
ds = sti_dataset
|
@@ -278,25 +285,30 @@ module Sequel
|
|
278
285
|
table = nil if !columns || columns.empty?
|
279
286
|
end
|
280
287
|
end
|
281
|
-
table = nil if table && (table ==
|
288
|
+
table = nil if table && (table == cti_table_name)
|
282
289
|
|
283
290
|
return unless table
|
284
291
|
|
285
292
|
pk = primary_key
|
286
293
|
subclass.instance_eval do
|
287
294
|
if cti_tables.length == 1
|
288
|
-
ds = ds.select(*self.columns.map{|cc| Sequel.qualify(
|
295
|
+
ds = ds.select(*self.columns.map{|cc| Sequel.qualify(cti_table_name, Sequel.identifier(cc))})
|
289
296
|
end
|
290
297
|
cols = columns - [pk]
|
291
298
|
unless (cols & ds.columns).empty?
|
292
299
|
Sequel::Deprecation.deprecate('Using class_table_inheritance with duplicate column names in subclass tables (other than the primary key column)', 'Make sure all tables used have unique column names, or implement support for handling duplicate column names in the class_table_inheritance plugin')
|
293
300
|
end
|
294
301
|
sel_app = cols.map{|cc| Sequel.qualify(table, Sequel.identifier(cc))}
|
295
|
-
@sti_dataset = ds.join(table, pk=>pk).select_append(*sel_app)
|
296
|
-
|
302
|
+
@sti_dataset = ds = ds.join(table, pk=>pk).select_append(*sel_app)
|
303
|
+
|
304
|
+
if @cti_alias
|
305
|
+
ds = ds.from_self(:alias=>@cti_alias)
|
306
|
+
end
|
307
|
+
|
308
|
+
set_dataset(ds)
|
297
309
|
set_columns(self.columns)
|
298
310
|
@dataset = @dataset.with_row_proc(lambda{|r| subclass.sti_load(r)})
|
299
|
-
cols.each{|a| define_lazy_attribute_getter(a, :dataset=>dataset, :table
|
311
|
+
cols.each{|a| define_lazy_attribute_getter(a, :dataset=>dataset, :table=>@cti_alias||table)}
|
300
312
|
|
301
313
|
@cti_models += [self]
|
302
314
|
@cti_tables += [table]
|
@@ -311,12 +323,37 @@ module Sequel
|
|
311
323
|
|
312
324
|
# The table name for the current model class's main table.
|
313
325
|
def table_name
|
314
|
-
|
326
|
+
if cti_tables
|
327
|
+
if @cti_alias
|
328
|
+
@cti_alias
|
329
|
+
else
|
330
|
+
cti_tables.last
|
331
|
+
end
|
332
|
+
else
|
333
|
+
super
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# The name of the most recently joined table.
|
338
|
+
def cti_table_name
|
339
|
+
cti_tables ? cti_tables.last : dataset.first_source_alias
|
315
340
|
end
|
316
341
|
|
317
342
|
def sti_class_from_key(key)
|
318
343
|
sti_class(sti_model_map[key])
|
319
344
|
end
|
345
|
+
|
346
|
+
private
|
347
|
+
|
348
|
+
# If using a subquery for class table inheritance, also use a subquery
|
349
|
+
# when setting subclass dataset.
|
350
|
+
def sti_subclass_dataset(key)
|
351
|
+
ds = super
|
352
|
+
if @cti_alias
|
353
|
+
ds = ds.from_self(:alias=>@cti_alias)
|
354
|
+
end
|
355
|
+
ds
|
356
|
+
end
|
320
357
|
end
|
321
358
|
|
322
359
|
module InstanceMethods
|
@@ -346,8 +383,8 @@ module Sequel
|
|
346
383
|
if new? && (set = self[model.sti_key])
|
347
384
|
exp = model.sti_key_chooser.call(self)
|
348
385
|
if set != exp
|
349
|
-
set_table = model.sti_class_from_key(set).
|
350
|
-
exp_table = model.sti_class_from_key(exp).
|
386
|
+
set_table = model.sti_class_from_key(set).cti_table_name
|
387
|
+
exp_table = model.sti_class_from_key(exp).cti_table_name
|
351
388
|
set_column_value("#{model.sti_key}=", exp) if set_table != exp_table
|
352
389
|
end
|
353
390
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Plugins
|
5
|
+
# The def_dataset_method plugin adds Model.def_dataset_method
|
6
|
+
# for defining dataset methods:
|
7
|
+
#
|
8
|
+
# Album.def_dataset_method(:by_name) do |name|
|
9
|
+
# where(:name=>name)
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# Additionally, this adds support for Model.subset, which can also
|
13
|
+
# be used to define dataset methods that add specific filters:
|
14
|
+
#
|
15
|
+
# Album.subset(:gold){copies_sold >= 500000}
|
16
|
+
#
|
17
|
+
# This exists for backwards compatibility with previous Sequel versions.
|
18
|
+
#
|
19
|
+
# Usage:
|
20
|
+
#
|
21
|
+
# # Make all model subclasses support Model.def_dataset_method
|
22
|
+
# # (called before loading subclasses)
|
23
|
+
# Sequel::Model.plugin :def_dataset_method
|
24
|
+
#
|
25
|
+
# # Make the Album class support Model.def_dataset_method
|
26
|
+
# Album.plugin :def_dataset_method
|
27
|
+
module DefDatasetMethod
|
28
|
+
module ClassMethods
|
29
|
+
# If a block is given, define a method on the dataset (if the model currently has an dataset) with the given argument name using
|
30
|
+
# the given block. Also define a class method on the model that calls the
|
31
|
+
# dataset method. Stores the method name and block so that it can be reapplied if the model's
|
32
|
+
# dataset changes.
|
33
|
+
#
|
34
|
+
# If a block is not given, just define a class method on the model for each argument
|
35
|
+
# that calls the dataset method of the same argument name.
|
36
|
+
#
|
37
|
+
# Using dataset_module is recommended over using this method. In addition to allowing
|
38
|
+
# more natural ruby syntax for defining methods manually, it also offers numerous
|
39
|
+
# helper methods that make defining common dataset methods more easily, as well as
|
40
|
+
# supporting dataset caching (assuming the arguments allow it).
|
41
|
+
#
|
42
|
+
# # Add new dataset method and class method that calls it
|
43
|
+
# Artist.def_dataset_method(:by_name){order(:name)}
|
44
|
+
# Artist.where(:name.like('A%')).by_name
|
45
|
+
# Artist.by_name.where(:name.like('A%'))
|
46
|
+
#
|
47
|
+
# # Just add a class method that calls an existing dataset method
|
48
|
+
# Artist.def_dataset_method(:paginate)
|
49
|
+
# Artist.paginate(2, 10)
|
50
|
+
def def_dataset_method(*args, &block)
|
51
|
+
raise(Error, "No arguments given") if args.empty?
|
52
|
+
|
53
|
+
if block
|
54
|
+
raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1
|
55
|
+
dataset_module{define_method(args.first, &block)}
|
56
|
+
else
|
57
|
+
args.each{|arg| def_model_dataset_method(arg)}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets up a dataset method that returns a filtered dataset.
|
62
|
+
# Sometimes thought of as a scope, and like most dataset methods,
|
63
|
+
# they can be chained.
|
64
|
+
# For example:
|
65
|
+
#
|
66
|
+
# Topic.subset(:joes, :username.like('%joe%'))
|
67
|
+
# Topic.subset(:popular){num_posts > 100}
|
68
|
+
# Topic.subset(:recent){created_on > Date.today - 7}
|
69
|
+
#
|
70
|
+
# Allows you to do:
|
71
|
+
#
|
72
|
+
# Topic.joes.recent.popular
|
73
|
+
#
|
74
|
+
# to get topics with a username that includes joe that
|
75
|
+
# have more than 100 posts and were created less than
|
76
|
+
# 7 days ago.
|
77
|
+
#
|
78
|
+
# Both the args given and the block are passed to <tt>Dataset#filter</tt>.
|
79
|
+
#
|
80
|
+
# This method creates dataset methods that do not accept arguments. To create
|
81
|
+
# dataset methods that accept arguments, you should use define a
|
82
|
+
# method directly inside a #dataset_module block.
|
83
|
+
def subset(*args, &block)
|
84
|
+
dataset_module{subset(*args, &block)}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Plugins
|
5
|
+
# The finder plugin adds Model.finder for defining optimized finder methods.
|
6
|
+
# There are two ways to use this. The recommended way is to pass a symbol
|
7
|
+
# that represents a model class method that returns a dataset:
|
8
|
+
#
|
9
|
+
# def Artist.by_name(name)
|
10
|
+
# where(:name=>name)
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Artist.finder :by_name
|
14
|
+
#
|
15
|
+
# This creates an optimized first_by_name method, which you can call normally:
|
16
|
+
#
|
17
|
+
# Artist.first_by_name("Joe")
|
18
|
+
#
|
19
|
+
# The alternative way to use this to pass your own block:
|
20
|
+
#
|
21
|
+
# Artist.finder(:name=>:first_by_name){|pl, ds| ds.where(:name=>pl.arg).limit(1)}
|
22
|
+
#
|
23
|
+
# Additionally, there is a Model.prepared_finder method. This works similarly
|
24
|
+
# to Model.finder, but uses a prepared statement. This limits the types of
|
25
|
+
# arguments that will be accepted, but can perform better in the database.
|
26
|
+
#
|
27
|
+
# Usage:
|
28
|
+
#
|
29
|
+
# # Make all model subclasses support Model.finder
|
30
|
+
# # (called before loading subclasses)
|
31
|
+
# Sequel::Model.plugin :finder
|
32
|
+
#
|
33
|
+
# # Make the Album class support Model.finder
|
34
|
+
# Album.plugin :finder
|
35
|
+
module Finder
|
36
|
+
FINDER_TYPES = [:first, :all, :each, :get].freeze
|
37
|
+
|
38
|
+
def self.apply(mod)
|
39
|
+
mod.instance_exec do
|
40
|
+
@finders ||= {}
|
41
|
+
@finder_loaders ||= {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
# Create an optimized finder method using a dataset placeholder literalizer.
|
47
|
+
# This pre-computes the SQL to use for the query, except for given arguments.
|
48
|
+
#
|
49
|
+
# There are two ways to use this. The recommended way is to pass a symbol
|
50
|
+
# that represents a model class method that returns a dataset:
|
51
|
+
#
|
52
|
+
# def Artist.by_name(name)
|
53
|
+
# where(:name=>name)
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Artist.finder :by_name
|
57
|
+
#
|
58
|
+
# This creates an optimized first_by_name method, which you can call normally:
|
59
|
+
#
|
60
|
+
# Artist.first_by_name("Joe")
|
61
|
+
#
|
62
|
+
# The alternative way to use this to pass your own block:
|
63
|
+
#
|
64
|
+
# Artist.finder(:name=>:first_by_name){|pl, ds| ds.where(:name=>pl.arg).limit(1)}
|
65
|
+
#
|
66
|
+
# Note that if you pass your own block, you are responsible for manually setting
|
67
|
+
# limits if necessary (as shown above).
|
68
|
+
#
|
69
|
+
# Options:
|
70
|
+
# :arity :: When using a symbol method name, this specifies the arity of the method.
|
71
|
+
# This should be used if if the method accepts an arbitrary number of arguments,
|
72
|
+
# or the method has default argument values. Note that if the method is defined
|
73
|
+
# as a dataset method, the class method Sequel creates accepts an arbitrary number
|
74
|
+
# of arguments, so you should use this option in that case. If you want to handle
|
75
|
+
# multiple possible arities, you need to call the finder method multiple times with
|
76
|
+
# unique :arity and :name methods each time.
|
77
|
+
# :name :: The name of the method to create. This must be given if you pass a block.
|
78
|
+
# If you use a symbol, this defaults to the symbol prefixed by the type.
|
79
|
+
# :mod :: The module in which to create the finder method. Defaults to the singleton
|
80
|
+
# class of the model.
|
81
|
+
# :type :: The type of query to run. Can be :first, :each, :all, or :get, defaults to
|
82
|
+
# :first.
|
83
|
+
#
|
84
|
+
# Caveats:
|
85
|
+
#
|
86
|
+
# This doesn't handle all possible cases. For example, if you have a method such as:
|
87
|
+
#
|
88
|
+
# def Artist.by_name(name)
|
89
|
+
# name ? where(:name=>name) : exclude(:name=>nil)
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# Then calling a finder without an argument will not work as you expect.
|
93
|
+
#
|
94
|
+
# Artist.finder :by_name
|
95
|
+
# Artist.by_name(nil).first
|
96
|
+
# # WHERE (name IS NOT NULL)
|
97
|
+
# Artist.first_by_name(nil)
|
98
|
+
# # WHERE (name IS NULL)
|
99
|
+
#
|
100
|
+
# See Dataset::PlaceholderLiteralizer for additional caveats.
|
101
|
+
def finder(meth=OPTS, opts=OPTS, &block)
|
102
|
+
if block
|
103
|
+
raise Error, "cannot pass both a method name argument and a block of Model.finder" unless meth.is_a?(Hash)
|
104
|
+
raise Error, "cannot pass two option hashes to Model.finder" unless opts.equal?(OPTS)
|
105
|
+
opts = meth
|
106
|
+
raise Error, "must provide method name via :name option when passing block to Model.finder" unless meth_name = opts[:name]
|
107
|
+
end
|
108
|
+
|
109
|
+
type = opts.fetch(:type, :first)
|
110
|
+
unless prepare = opts[:prepare]
|
111
|
+
raise Error, ":type option to Model.finder must be :first, :all, :each, or :get" unless FINDER_TYPES.include?(type)
|
112
|
+
end
|
113
|
+
limit1 = type == :first || type == :get
|
114
|
+
meth_name ||= opts[:name] || :"#{type}_#{meth}"
|
115
|
+
|
116
|
+
argn = lambda do |model|
|
117
|
+
if arity = opts[:arity]
|
118
|
+
arity
|
119
|
+
else
|
120
|
+
method = block || model.method(meth)
|
121
|
+
(method.arity < 0 ? method.arity.abs - 1 : method.arity)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
loader_proc = if prepare
|
126
|
+
proc do |model|
|
127
|
+
args = prepare_method_args('$a', argn.call(model))
|
128
|
+
ds = if block
|
129
|
+
model.instance_exec(*args, &block)
|
130
|
+
else
|
131
|
+
model.send(meth, *args)
|
132
|
+
end
|
133
|
+
ds = ds.limit(1) if limit1
|
134
|
+
model_name = model.name
|
135
|
+
if model_name.to_s.empty?
|
136
|
+
model_name = model.object_id
|
137
|
+
else
|
138
|
+
model_name = model_name.gsub(/\W/, '_')
|
139
|
+
end
|
140
|
+
ds.prepare(type, :"#{model_name}_#{meth_name}")
|
141
|
+
end
|
142
|
+
else
|
143
|
+
proc do |model|
|
144
|
+
n = argn.call(model)
|
145
|
+
block ||= lambda do |pl, model2|
|
146
|
+
args = (0...n).map{pl.arg}
|
147
|
+
ds = model2.send(meth, *args)
|
148
|
+
ds = ds.limit(1) if limit1
|
149
|
+
ds
|
150
|
+
end
|
151
|
+
|
152
|
+
Sequel::Dataset::PlaceholderLiteralizer.loader(model, &block)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
@finder_loaders[meth_name] = loader_proc
|
157
|
+
mod = opts[:mod] || (class << self; self; end)
|
158
|
+
if prepare
|
159
|
+
def_prepare_method(mod, meth_name)
|
160
|
+
else
|
161
|
+
def_finder_method(mod, meth_name, type)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def freeze
|
166
|
+
@finder_loaders.freeze
|
167
|
+
@finder_loaders.each_key{|k| finder_for(k)} if @dataset
|
168
|
+
@finders.freeze
|
169
|
+
super
|
170
|
+
end
|
171
|
+
|
172
|
+
# Similar to finder, but uses a prepared statement instead of a placeholder
|
173
|
+
# literalizer. This makes the SQL used static (cannot vary per call), but
|
174
|
+
# allows binding argument values instead of literalizing them into the SQL
|
175
|
+
# query string.
|
176
|
+
#
|
177
|
+
# If a block is used with this method, it is instance_execed by the model,
|
178
|
+
# and should accept the desired number of placeholder arguments.
|
179
|
+
#
|
180
|
+
# The options are the same as the options for finder, with the following
|
181
|
+
# exception:
|
182
|
+
# :type :: Specifies the type of prepared statement to create
|
183
|
+
def prepared_finder(meth=OPTS, opts=OPTS, &block)
|
184
|
+
if block
|
185
|
+
raise Error, "cannot pass both a method name argument and a block of Model.finder" unless meth.is_a?(Hash)
|
186
|
+
meth = meth.merge(:prepare=>true)
|
187
|
+
else
|
188
|
+
opts = opts.merge(:prepare=>true)
|
189
|
+
end
|
190
|
+
finder(meth, opts, &block)
|
191
|
+
end
|
192
|
+
|
193
|
+
Plugins.inherited_instance_variables(self, :@finders=>:dup, :@autoreloading_associations=>:hash_dup, :@default_association_options=>:dup, :@cache_associations=>nil, :@default_eager_limit_strategy=>nil)
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
# Define a finder method in the given module with the given method name that
|
198
|
+
# load rows using the finder with the given name.
|
199
|
+
def def_finder_method(mod, meth, type)
|
200
|
+
mod.send(:define_method, meth){|*args, &block| finder_for(meth).send(type, *args, &block)}
|
201
|
+
end
|
202
|
+
|
203
|
+
# Define a prepared_finder method in the given module that will call the associated prepared
|
204
|
+
# statement.
|
205
|
+
def def_prepare_method(mod, meth)
|
206
|
+
mod.send(:define_method, meth){|*args, &block| finder_for(meth).call(prepare_method_arg_hash(args), &block)}
|
207
|
+
end
|
208
|
+
|
209
|
+
# Find the finder to use for the give method. If a finder has not been loaded
|
210
|
+
# for the method, load the finder and set correctly in the finders hash, then
|
211
|
+
# return the finder.
|
212
|
+
def finder_for(meth)
|
213
|
+
unless finder = (frozen? ? @finders[meth] : Sequel.synchronize{@finders[meth]})
|
214
|
+
finder_loader = @finder_loaders.fetch(meth)
|
215
|
+
finder = finder_loader.call(self)
|
216
|
+
Sequel.synchronize{@finders[meth] = finder}
|
217
|
+
end
|
218
|
+
finder
|
219
|
+
end
|
220
|
+
|
221
|
+
# An hash of prepared argument values for the given arguments, with keys
|
222
|
+
# starting at a. Used by the methods created by prepared_finder.
|
223
|
+
def prepare_method_arg_hash(args)
|
224
|
+
h = {}
|
225
|
+
prepare_method_args('a', args.length).zip(args).each{|k, v| h[k] = v}
|
226
|
+
h
|
227
|
+
end
|
228
|
+
|
229
|
+
# An array of prepared statement argument names, of length n and starting with base.
|
230
|
+
def prepare_method_args(base, n)
|
231
|
+
(0...n).map do
|
232
|
+
s = base.to_sym
|
233
|
+
base = base.next
|
234
|
+
s
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|