sequel 4.45.0 → 4.46.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +108 -0
  3. data/doc/release_notes/4.46.0.txt +404 -0
  4. data/doc/security.rdoc +9 -0
  5. data/doc/sql.rdoc +2 -2
  6. data/doc/testing.rdoc +1 -1
  7. data/doc/validations.rdoc +1 -2
  8. data/lib/sequel/adapters/ado.rb +8 -3
  9. data/lib/sequel/adapters/ado/access.rb +8 -4
  10. data/lib/sequel/adapters/ado/mssql.rb +3 -1
  11. data/lib/sequel/adapters/amalgalite.rb +5 -0
  12. data/lib/sequel/adapters/cubrid.rb +16 -7
  13. data/lib/sequel/adapters/do.rb +7 -1
  14. data/lib/sequel/adapters/do/mysql.rb +8 -4
  15. data/lib/sequel/adapters/ibmdb.rb +10 -5
  16. data/lib/sequel/adapters/jdbc.rb +8 -2
  17. data/lib/sequel/adapters/jdbc/as400.rb +10 -3
  18. data/lib/sequel/adapters/jdbc/db2.rb +27 -16
  19. data/lib/sequel/adapters/jdbc/derby.rb +47 -20
  20. data/lib/sequel/adapters/jdbc/h2.rb +13 -7
  21. data/lib/sequel/adapters/jdbc/hsqldb.rb +18 -9
  22. data/lib/sequel/adapters/jdbc/mssql.rb +5 -2
  23. data/lib/sequel/adapters/jdbc/mysql.rb +3 -2
  24. data/lib/sequel/adapters/jdbc/oracle.rb +3 -2
  25. data/lib/sequel/adapters/jdbc/postgresql.rb +4 -3
  26. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +2 -1
  27. data/lib/sequel/adapters/jdbc/sqlite.rb +10 -3
  28. data/lib/sequel/adapters/jdbc/sqlserver.rb +23 -0
  29. data/lib/sequel/adapters/jdbc/transactions.rb +16 -10
  30. data/lib/sequel/adapters/mock.rb +5 -0
  31. data/lib/sequel/adapters/mysql.rb +8 -1
  32. data/lib/sequel/adapters/mysql2.rb +6 -1
  33. data/lib/sequel/adapters/odbc.rb +20 -8
  34. data/lib/sequel/adapters/odbc/mssql.rb +6 -3
  35. data/lib/sequel/adapters/oracle.rb +12 -6
  36. data/lib/sequel/adapters/postgres.rb +20 -8
  37. data/lib/sequel/adapters/shared/access.rb +76 -47
  38. data/lib/sequel/adapters/shared/cubrid.rb +16 -11
  39. data/lib/sequel/adapters/shared/db2.rb +46 -19
  40. data/lib/sequel/adapters/shared/firebird.rb +20 -8
  41. data/lib/sequel/adapters/shared/informix.rb +6 -3
  42. data/lib/sequel/adapters/shared/mssql.rb +132 -72
  43. data/lib/sequel/adapters/shared/mysql.rb +112 -65
  44. data/lib/sequel/adapters/shared/oracle.rb +36 -21
  45. data/lib/sequel/adapters/shared/postgres.rb +91 -56
  46. data/lib/sequel/adapters/shared/sqlanywhere.rb +65 -37
  47. data/lib/sequel/adapters/shared/sqlite.rb +67 -32
  48. data/lib/sequel/adapters/sqlanywhere.rb +9 -1
  49. data/lib/sequel/adapters/sqlite.rb +8 -1
  50. data/lib/sequel/adapters/swift.rb +5 -0
  51. data/lib/sequel/adapters/swift/mysql.rb +4 -2
  52. data/lib/sequel/adapters/swift/sqlite.rb +1 -1
  53. data/lib/sequel/adapters/tinytds.rb +10 -3
  54. data/lib/sequel/adapters/utils/emulate_offset_with_reverse_and_count.rb +1 -1
  55. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +1 -1
  56. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -0
  57. data/lib/sequel/adapters/utils/pg_types.rb +14 -6
  58. data/lib/sequel/adapters/utils/replace.rb +4 -2
  59. data/lib/sequel/connection_pool/single.rb +2 -2
  60. data/lib/sequel/core.rb +24 -11
  61. data/lib/sequel/database/connecting.rb +9 -3
  62. data/lib/sequel/database/dataset_defaults.rb +7 -1
  63. data/lib/sequel/database/logging.rb +1 -0
  64. data/lib/sequel/database/misc.rb +5 -2
  65. data/lib/sequel/database/query.rb +7 -5
  66. data/lib/sequel/database/schema_generator.rb +1 -0
  67. data/lib/sequel/database/schema_methods.rb +50 -27
  68. data/lib/sequel/database/transactions.rb +19 -9
  69. data/lib/sequel/dataset/actions.rb +15 -6
  70. data/lib/sequel/dataset/graph.rb +15 -5
  71. data/lib/sequel/dataset/misc.rb +12 -4
  72. data/lib/sequel/dataset/mutation.rb +17 -8
  73. data/lib/sequel/dataset/prepared_statements.rb +3 -2
  74. data/lib/sequel/dataset/query.rb +84 -38
  75. data/lib/sequel/dataset/sql.rb +302 -191
  76. data/lib/sequel/deprecated.rb +26 -17
  77. data/lib/sequel/extensions/_deprecated_identifier_mangling.rb +2 -2
  78. data/lib/sequel/extensions/auto_literal_strings.rb +74 -0
  79. data/lib/sequel/extensions/from_block.rb +1 -0
  80. data/lib/sequel/extensions/graph_each.rb +1 -1
  81. data/lib/sequel/extensions/identifier_mangling.rb +2 -2
  82. data/lib/sequel/extensions/migration.rb +28 -4
  83. data/lib/sequel/extensions/no_auto_literal_strings.rb +2 -0
  84. data/lib/sequel/extensions/schema_dumper.rb +4 -4
  85. data/lib/sequel/extensions/sequel_3_dataset_methods.rb +5 -3
  86. data/lib/sequel/extensions/set_overrides.rb +2 -0
  87. data/lib/sequel/extensions/split_array_nil.rb +2 -2
  88. data/lib/sequel/extensions/virtual_row_method_block.rb +44 -0
  89. data/lib/sequel/model.rb +11 -7
  90. data/lib/sequel/model/associations.rb +5 -7
  91. data/lib/sequel/model/base.rb +47 -45
  92. data/lib/sequel/model/dataset_module.rb +9 -14
  93. data/lib/sequel/model/plugins.rb +3 -0
  94. data/lib/sequel/no_core_ext.rb +1 -0
  95. data/lib/sequel/plugins/blacklist_security.rb +1 -1
  96. data/lib/sequel/plugins/boolean_subsets.rb +7 -5
  97. data/lib/sequel/plugins/class_table_inheritance.rb +47 -10
  98. data/lib/sequel/plugins/dataset_associations.rb +1 -1
  99. data/lib/sequel/plugins/def_dataset_method.rb +90 -0
  100. data/lib/sequel/plugins/finder.rb +240 -0
  101. data/lib/sequel/plugins/inverted_subsets.rb +19 -12
  102. data/lib/sequel/plugins/many_through_many.rb +1 -1
  103. data/lib/sequel/plugins/nested_attributes.rb +1 -1
  104. data/lib/sequel/plugins/schema.rb +1 -1
  105. data/lib/sequel/plugins/single_table_inheritance.rb +7 -1
  106. data/lib/sequel/plugins/subset_conditions.rb +11 -3
  107. data/lib/sequel/plugins/whitelist_security.rb +118 -0
  108. data/lib/sequel/sql.rb +80 -36
  109. data/lib/sequel/timezones.rb +2 -0
  110. data/lib/sequel/version.rb +1 -1
  111. data/spec/adapters/mssql_spec.rb +20 -0
  112. data/spec/adapters/mysql_spec.rb +1 -1
  113. data/spec/adapters/oracle_spec.rb +12 -8
  114. data/spec/adapters/postgres_spec.rb +1 -1
  115. data/spec/adapters/spec_helper.rb +1 -1
  116. data/spec/adapters/sqlite_spec.rb +36 -34
  117. data/spec/core/connection_pool_spec.rb +2 -1
  118. data/spec/core/database_spec.rb +87 -9
  119. data/spec/core/dataset_spec.rb +501 -129
  120. data/spec/core/deprecated_spec.rb +1 -1
  121. data/spec/core/expression_filters_spec.rb +146 -60
  122. data/spec/core/mock_adapter_spec.rb +1 -1
  123. data/spec/core/object_graph_spec.rb +61 -9
  124. data/spec/core/placeholder_literalizer_spec.rb +20 -2
  125. data/spec/core/schema_generator_spec.rb +6 -6
  126. data/spec/core/schema_spec.rb +54 -5
  127. data/spec/core_extensions_spec.rb +122 -18
  128. data/spec/deprecation_helper.rb +27 -2
  129. data/spec/extensions/_deprecated_identifier_mangling_spec.rb +6 -6
  130. data/spec/extensions/association_proxies_spec.rb +2 -2
  131. data/spec/extensions/auto_literal_strings_spec.rb +212 -0
  132. data/spec/extensions/blacklist_security_spec.rb +1 -0
  133. data/spec/extensions/class_table_inheritance_spec.rb +1037 -39
  134. data/spec/extensions/column_select_spec.rb +20 -8
  135. data/spec/extensions/columns_introspection_spec.rb +3 -3
  136. data/spec/extensions/core_refinements_spec.rb +29 -12
  137. data/spec/extensions/dataset_associations_spec.rb +12 -12
  138. data/spec/extensions/def_dataset_method_spec.rb +100 -0
  139. data/spec/extensions/error_sql_spec.rb +1 -1
  140. data/spec/extensions/finder_spec.rb +260 -0
  141. data/spec/extensions/graph_each_spec.rb +2 -2
  142. data/spec/extensions/identifier_mangling_spec.rb +14 -8
  143. data/spec/extensions/inverted_subsets_spec.rb +4 -4
  144. data/spec/extensions/lazy_attributes_spec.rb +7 -0
  145. data/spec/extensions/many_through_many_spec.rb +38 -14
  146. data/spec/extensions/nested_attributes_spec.rb +18 -6
  147. data/spec/extensions/no_auto_literal_strings_spec.rb +1 -1
  148. data/spec/extensions/pg_enum_spec.rb +16 -1
  149. data/spec/extensions/pg_interval_spec.rb +11 -2
  150. data/spec/extensions/pg_loose_count_spec.rb +5 -0
  151. data/spec/extensions/pg_row_spec.rb +25 -0
  152. data/spec/extensions/prepared_statements_spec.rb +10 -1
  153. data/spec/extensions/query_spec.rb +2 -2
  154. data/spec/extensions/schema_dumper_spec.rb +2 -2
  155. data/spec/extensions/schema_spec.rb +2 -2
  156. data/spec/extensions/set_overrides_spec.rb +7 -3
  157. data/spec/extensions/sql_expr_spec.rb +0 -1
  158. data/spec/extensions/subset_conditions_spec.rb +6 -6
  159. data/spec/extensions/table_select_spec.rb +24 -12
  160. data/spec/extensions/to_dot_spec.rb +4 -4
  161. data/spec/extensions/whitelist_security_spec.rb +131 -0
  162. data/spec/integration/dataset_test.rb +9 -5
  163. data/spec/integration/model_test.rb +2 -0
  164. data/spec/integration/plugin_test.rb +2 -2
  165. data/spec/integration/spec_helper.rb +1 -1
  166. data/spec/model/associations_spec.rb +39 -11
  167. data/spec/model/base_spec.rb +44 -24
  168. data/spec/model/class_dataset_methods_spec.rb +18 -16
  169. data/spec/model/dataset_methods_spec.rb +4 -4
  170. data/spec/model/eager_loading_spec.rb +84 -24
  171. data/spec/model/model_spec.rb +97 -63
  172. data/spec/model/record_spec.rb +21 -13
  173. 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.table_name] = m.cti_table_columns }
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 == table_name)
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(table_name, Sequel.identifier(cc))})
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
- set_dataset(@sti_dataset)
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=>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
- cti_tables ? cti_tables.last : super
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).table_name
350
- exp_table = model.sti_class_from_key(exp).table_name
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
@@ -62,7 +62,7 @@ module Sequel
62
62
  ret = super
63
63
  r = association_reflection(name)
64
64
  meth = r.returns_array? ? name : pluralize(name).to_sym
65
- def_dataset_method(meth){associated(name)}
65
+ dataset_module{define_method(meth){associated(name)}}
66
66
  ret
67
67
  end
68
68
 
@@ -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