sequel 3.21.0 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. data/CHANGELOG +112 -0
  2. data/README.rdoc +15 -1
  3. data/doc/association_basics.rdoc +159 -40
  4. data/doc/model_hooks.rdoc +64 -27
  5. data/doc/prepared_statements.rdoc +8 -4
  6. data/doc/reflection.rdoc +8 -2
  7. data/doc/release_notes/3.22.0.txt +39 -0
  8. data/doc/release_notes/3.23.0.txt +172 -0
  9. data/doc/release_notes/3.24.0.txt +420 -0
  10. data/doc/virtual_rows.rdoc +2 -2
  11. data/lib/sequel/adapters/ado.rb +2 -1
  12. data/lib/sequel/adapters/db2.rb +8 -1
  13. data/lib/sequel/adapters/firebird.rb +25 -9
  14. data/lib/sequel/adapters/informix.rb +4 -19
  15. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  16. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  17. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  18. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  19. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  20. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  21. data/lib/sequel/adapters/jdbc.rb +39 -20
  22. data/lib/sequel/adapters/odbc.rb +2 -0
  23. data/lib/sequel/adapters/oracle.rb +12 -0
  24. data/lib/sequel/adapters/postgres.rb +30 -1
  25. data/lib/sequel/adapters/shared/access.rb +10 -0
  26. data/lib/sequel/adapters/shared/informix.rb +45 -0
  27. data/lib/sequel/adapters/shared/mssql.rb +106 -11
  28. data/lib/sequel/adapters/shared/mysql.rb +25 -7
  29. data/lib/sequel/adapters/shared/postgres.rb +39 -6
  30. data/lib/sequel/adapters/shared/sqlite.rb +57 -5
  31. data/lib/sequel/adapters/sqlite.rb +8 -3
  32. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  33. data/lib/sequel/adapters/tinytds.rb +4 -3
  34. data/lib/sequel/ast_transformer.rb +190 -0
  35. data/lib/sequel/core.rb +1 -1
  36. data/lib/sequel/database/connecting.rb +1 -1
  37. data/lib/sequel/database/misc.rb +6 -0
  38. data/lib/sequel/database/query.rb +33 -3
  39. data/lib/sequel/database/schema_methods.rb +13 -4
  40. data/lib/sequel/dataset/features.rb +6 -0
  41. data/lib/sequel/dataset/prepared_statements.rb +17 -2
  42. data/lib/sequel/dataset/query.rb +17 -0
  43. data/lib/sequel/dataset/sql.rb +2 -53
  44. data/lib/sequel/exceptions.rb +4 -0
  45. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  46. data/lib/sequel/extensions/migration.rb +4 -3
  47. data/lib/sequel/extensions/to_dot.rb +95 -83
  48. data/lib/sequel/model/associations.rb +234 -32
  49. data/lib/sequel/model/base.rb +187 -60
  50. data/lib/sequel/model/exceptions.rb +3 -1
  51. data/lib/sequel/model.rb +5 -0
  52. data/lib/sequel/plugins/association_pks.rb +6 -4
  53. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  54. data/lib/sequel/plugins/identity_map.rb +2 -2
  55. data/lib/sequel/plugins/many_through_many.rb +33 -3
  56. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  57. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  58. data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
  59. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  60. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  61. data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
  62. data/lib/sequel/plugins/xml_serializer.rb +1 -1
  63. data/lib/sequel/sql.rb +8 -0
  64. data/lib/sequel/version.rb +1 -1
  65. data/spec/adapters/mssql_spec.rb +36 -0
  66. data/spec/adapters/mysql_spec.rb +6 -0
  67. data/spec/adapters/postgres_spec.rb +43 -18
  68. data/spec/adapters/spec_helper.rb +5 -0
  69. data/spec/core/connection_pool_spec.rb +56 -77
  70. data/spec/core/database_spec.rb +33 -0
  71. data/spec/core/dataset_spec.rb +127 -16
  72. data/spec/core/expression_filters_spec.rb +13 -0
  73. data/spec/core/schema_spec.rb +13 -1
  74. data/spec/core/spec_helper.rb +5 -0
  75. data/spec/extensions/association_pks_spec.rb +7 -0
  76. data/spec/extensions/columns_introspection_spec.rb +91 -0
  77. data/spec/extensions/defaults_setter_spec.rb +64 -0
  78. data/spec/extensions/many_through_many_spec.rb +77 -0
  79. data/spec/extensions/migration_spec.rb +17 -17
  80. data/spec/extensions/nested_attributes_spec.rb +1 -0
  81. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  82. data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
  83. data/spec/extensions/prepared_statements_spec.rb +72 -0
  84. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  85. data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
  86. data/spec/extensions/single_table_inheritance_spec.rb +11 -0
  87. data/spec/extensions/spec_helper.rb +3 -1
  88. data/spec/extensions/to_dot_spec.rb +3 -5
  89. data/spec/extensions/xml_serializer_spec.rb +12 -0
  90. data/spec/integration/associations_test.rb +212 -0
  91. data/spec/integration/dataset_test.rb +8 -1
  92. data/spec/integration/plugin_test.rb +134 -0
  93. data/spec/integration/prepared_statement_test.rb +72 -1
  94. data/spec/integration/schema_test.rb +66 -8
  95. data/spec/integration/spec_helper.rb +5 -0
  96. data/spec/integration/transaction_test.rb +40 -0
  97. data/spec/integration/type_test.rb +7 -0
  98. data/spec/model/associations_spec.rb +463 -5
  99. data/spec/model/base_spec.rb +59 -0
  100. data/spec/model/eager_loading_spec.rb +269 -1
  101. data/spec/model/hooks_spec.rb +161 -0
  102. data/spec/model/record_spec.rb +30 -0
  103. data/spec/model/spec_helper.rb +5 -0
  104. metadata +29 -4
@@ -0,0 +1,190 @@
1
+ module Sequel
2
+ # The +ASTTransformer+ class is designed to handle the abstract syntax trees
3
+ # that Sequel uses internally and produce modified copies of them. By itself
4
+ # it only produces a straight copy. It's designed to be subclassed and have
5
+ # subclasses returned modified copies of the specific nodes that need to
6
+ # be modified.
7
+ class ASTTransformer
8
+ # Return +obj+ or a potentially transformed version of it.
9
+ def transform(obj)
10
+ v(obj)
11
+ end
12
+
13
+ private
14
+
15
+ # Recursive version that handles all of Sequel's internal object types
16
+ # and produces copies of them.
17
+ def v(o)
18
+ case o
19
+ when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass
20
+ o
21
+ when Array
22
+ o.map{|x| v(x)}
23
+ when Hash
24
+ h = {}
25
+ o.each{|k, val| h[v(k)] = v(val)}
26
+ h
27
+ when SQL::ComplexExpression
28
+ SQL::ComplexExpression.new(o.op, *v(o.args))
29
+ when SQL::Identifier
30
+ SQL::Identifier.new(v(o.value))
31
+ when SQL::QualifiedIdentifier
32
+ SQL::QualifiedIdentifier.new(v(o.table), v(o.column))
33
+ when SQL::OrderedExpression
34
+ SQL::OrderedExpression.new(v(o.expression), o.descending, :nulls=>o.nulls)
35
+ when SQL::AliasedExpression
36
+ SQL::AliasedExpression.new(v(o.expression), o.aliaz)
37
+ when SQL::CaseExpression
38
+ args = [v(o.conditions), v(o.default)]
39
+ args << v(o.expression) if o.expression?
40
+ SQL::CaseExpression.new(*args)
41
+ when SQL::Cast
42
+ SQL::Cast.new(v(o.expr), o.type)
43
+ when SQL::Function
44
+ SQL::Function.new(o.f, *v(o.args))
45
+ when SQL::Subscript
46
+ SQL::Subscript.new(v(o.f), v(o.sub))
47
+ when SQL::WindowFunction
48
+ SQL::WindowFunction.new(v(o.function), v(o.window))
49
+ when SQL::Window
50
+ opts = o.opts.dup
51
+ opts[:partition] = v(opts[:partition]) if opts[:partition]
52
+ opts[:order] = v(opts[:order]) if opts[:order]
53
+ SQL::Window.new(opts)
54
+ when SQL::PlaceholderLiteralString
55
+ args = if o.args.is_a?(Hash)
56
+ h = {}
57
+ o.args.each{|k,val| h[k] = v(val)}
58
+ h
59
+ else
60
+ v(o.args)
61
+ end
62
+ SQL::PlaceholderLiteralString.new(o.str, args, o.parens)
63
+ when SQL::JoinOnClause
64
+ SQL::JoinOnClause.new(v(o.on), o.join_type, v(o.table), v(o.table_alias))
65
+ when SQL::JoinUsingClause
66
+ SQL::JoinUsingClause.new(v(o.using), o.join_type, v(o.table), v(o.table_alias))
67
+ when SQL::JoinClause
68
+ SQL::JoinClause.new(o.join_type, v(o.table), v(o.table_alias))
69
+ else
70
+ o
71
+ end
72
+ end
73
+ end
74
+
75
+ # Handles qualifying existing datasets, so that unqualified columns
76
+ # in the dataset are qualified with a given table name.
77
+ class Qualifier < ASTTransformer
78
+ # Store the dataset to use as the basis for qualification,
79
+ # and the table used to qualify unqualified columns.
80
+ def initialize(ds, table)
81
+ @ds = ds
82
+ @table = table
83
+ end
84
+
85
+ private
86
+
87
+ # Turn <tt>SQL::Identifier</tt>s and symbols that aren't implicitly
88
+ # qualified into <tt>SQL::QualifiedIdentifier</tt>s. For symbols that
89
+ # are not implicitly qualified by are implicitly aliased, return an
90
+ # <tt>SQL::AliasedExpression</tt>s with a qualified version of the symbol.
91
+ def v(o)
92
+ case o
93
+ when Symbol
94
+ t, column, aliaz = @ds.send(:split_symbol, o)
95
+ if t
96
+ o
97
+ elsif aliaz
98
+ SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(@table, SQL::Identifier.new(column)), aliaz)
99
+ else
100
+ SQL::QualifiedIdentifier.new(@table, o)
101
+ end
102
+ when SQL::Identifier
103
+ SQL::QualifiedIdentifier.new(@table, o)
104
+ when SQL::QualifiedIdentifier, SQL::JoinClause
105
+ # Return these directly, so we don't accidentally qualify symbols in them.
106
+ o
107
+ else
108
+ super
109
+ end
110
+ end
111
+ end
112
+
113
+ # +Unbinder+ is used to take a dataset filter and return a modified version
114
+ # that unbinds already bound values and returns a dataset with bound value
115
+ # placeholders and a hash of bind values. You can then prepare the dataset
116
+ # and use the bound variables to execute it with the same values.
117
+ #
118
+ # This class only does a limited form of unbinding where the variable names
119
+ # and values can be associated unambiguously. The only cases it handles
120
+ # are <tt>SQL::ComplexExpression<tt> with an operator in +UNBIND_OPS+, a
121
+ # first argument that's an instance of a member of +UNBIND_KEY_CLASSES+, and
122
+ # a second argument that's an instance of a member of +UNBIND_VALUE_CLASSES+.
123
+ #
124
+ # So it can handle cases like:
125
+ #
126
+ # DB.filter(:a=>1).exclude(:b=>2).where{c > 3}
127
+ #
128
+ # But it cannot handle cases like:
129
+ #
130
+ # DB.filter(:a + 1 < 0)
131
+ class Unbinder < ASTTransformer
132
+ # The <tt>SQL::ComplexExpression<tt> operates that will be considered
133
+ # for transformation.
134
+ UNBIND_OPS = [:'=', :'!=', :<, :>, :<=, :>=]
135
+
136
+ # The key classes (first argument of the ComplexExpression) that will
137
+ # considered for transformation.
138
+ UNBIND_KEY_CLASSES = [Symbol, SQL::Identifier, SQL::QualifiedIdentifier]
139
+
140
+ # The value classes (second argument of the ComplexExpression) that
141
+ # will be considered for transformation.
142
+ UNBIND_VALUE_CLASSES = [Numeric, String, Date, Time]
143
+
144
+ # The hash of bind variables that were extracted from the dataset filter.
145
+ attr_reader :binds
146
+
147
+ # Intialize an empty +binds+ hash.
148
+ def initialize
149
+ @binds = {}
150
+ end
151
+
152
+ private
153
+
154
+ # Create a suitable bound variable key for the object, which should be
155
+ # an instance of one of the +UNBIND_KEY_CLASSES+.
156
+ def bind_key(obj)
157
+ case obj
158
+ when Symbol, String
159
+ obj
160
+ when SQL::Identifier
161
+ bind_key(obj.value)
162
+ when SQL::QualifiedIdentifier
163
+ :"#{bind_key(obj.table)}.#{bind_key(obj.column)}"
164
+ else
165
+ raise Error, "unhandled object in Sequel::Unbinder#bind_key: #{obj}"
166
+ end
167
+ end
168
+
169
+ # Handle <tt>SQL::ComplexExpression</tt> instances with suitable ops
170
+ # and arguments, substituting the value with a bound variable placeholder
171
+ # and assigning it an entry in the +binds+ hash with a matching key.
172
+ def v(o)
173
+ if o.is_a?(SQL::ComplexExpression) && UNBIND_OPS.include?(o.op)
174
+ l, r = o.args
175
+ if UNBIND_KEY_CLASSES.any?{|c| l.is_a?(c)} && UNBIND_VALUE_CLASSES.any?{|c| r.is_a?(c)} && !r.is_a?(LiteralString)
176
+ key = bind_key(l)
177
+ if (old = binds[key]) && old != r
178
+ raise UnbindDuplicate, "two different values for #{key.inspect}: #{[r, old].inspect}"
179
+ end
180
+ binds[key] = r
181
+ SQL::ComplexExpression.new(o.op, l, :"$#{key}")
182
+ else
183
+ super
184
+ end
185
+ else
186
+ super
187
+ end
188
+ end
189
+ end
190
+ end
data/lib/sequel/core.rb CHANGED
@@ -292,7 +292,7 @@ module Sequel
292
292
 
293
293
  private_class_method :adapter_method, :def_adapter_method
294
294
 
295
- require(%w"metaprogramming sql connection_pool exceptions dataset database timezones version")
295
+ require(%w"metaprogramming sql connection_pool exceptions dataset database timezones ast_transformer version")
296
296
  require('core_sql') if !defined?(::SEQUEL_NO_CORE_EXTENSIONS) && !ENV.has_key?('SEQUEL_NO_CORE_EXTENSIONS')
297
297
 
298
298
  # Add the database adapter class methods to Sequel via metaprogramming
@@ -54,7 +54,7 @@ module Sequel
54
54
  c = adapter_class(scheme)
55
55
  uri_options = c.send(:uri_to_options, uri)
56
56
  uri.query.split('&').collect{|s| s.split('=')}.each{|k,v| uri_options[k.to_sym] = v if k && !k.empty?} unless uri.query.to_s.strip.empty?
57
- uri_options.entries.each{|k,v| uri_options[k] = URI.unescape(v) if v.is_a?(String)}
57
+ uri_options.to_a.each{|k,v| uri_options[k] = URI.unescape(v) if v.is_a?(String)}
58
58
  opts = uri_options.merge(opts)
59
59
  opts[:adapter] = scheme
60
60
  end
@@ -90,6 +90,12 @@ module Sequel
90
90
  {:primary_key => true, :type => Integer, :auto_increment => true}
91
91
  end
92
92
 
93
+ # Whether the database supports CREATE TABLE IF NOT EXISTS syntax,
94
+ # false by default.
95
+ def supports_create_table_if_not_exists?
96
+ false
97
+ end
98
+
93
99
  # Whether the database and adapter support prepared transactions
94
100
  # (two-phase commit), false by default.
95
101
  def supports_prepared_transactions?
@@ -210,6 +210,13 @@ module Sequel
210
210
  end
211
211
  end
212
212
 
213
+ # Return all views in the database as an array of symbols.
214
+ #
215
+ # DB.views # => [:gold_albums, :artists_with_many_albums]
216
+ def views(opts={})
217
+ raise NotImplemented, "#views should be overridden by adapters"
218
+ end
219
+
213
220
  private
214
221
 
215
222
  # Internal generic transaction method. Any exception raised by the given
@@ -226,7 +233,7 @@ module Sequel
226
233
  transaction_error(e)
227
234
  ensure
228
235
  begin
229
- commit_transaction(t, opts) unless e
236
+ commit_or_rollback_transaction(e, t, opts)
230
237
  rescue Exception => e
231
238
  raise_error(e, :classes=>database_error_classes)
232
239
  ensure
@@ -234,7 +241,7 @@ module Sequel
234
241
  end
235
242
  end
236
243
  end
237
-
244
+
238
245
  # Add the current thread to the list of active transactions
239
246
  def add_transaction
240
247
  th = Thread.current
@@ -335,6 +342,29 @@ module Sequel
335
342
  end
336
343
  end
337
344
 
345
+ if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
346
+ # Whether to commit the current transaction. On ruby 1.8 and rubinius,
347
+ # Thread.current.status is checked because Thread#kill skips rescue
348
+ # blocks (so exception would be nil), but the transaction should
349
+ # still be rolled back.
350
+ def commit_or_rollback_transaction(exception, thread, opts)
351
+ unless exception
352
+ if Thread.current.status == 'aborting'
353
+ rollback_transaction(thread, opts)
354
+ else
355
+ commit_transaction(thread, opts)
356
+ end
357
+ end
358
+ end
359
+ else
360
+ # Whether to commit the current transaction. On ruby 1.9 and JRuby,
361
+ # transactions will be committed if Thread#kill is used on an thread
362
+ # that has a transaction open, and there isn't a work around.
363
+ def commit_or_rollback_transaction(exception, thread, opts)
364
+ commit_transaction(thread, opts) unless exception
365
+ end
366
+ end
367
+
338
368
  # SQL to commit a savepoint
339
369
  def commit_savepoint_sql(depth)
340
370
  SQL_RELEASE_SAVEPOINT % depth
@@ -434,7 +464,7 @@ module Sequel
434
464
  :datetime
435
465
  when /\Atime( with(out)? time zone)?\z/io
436
466
  :time
437
- when /\A(boolean|bit)\z/io
467
+ when /\A(bool(ean)?)\z/io
438
468
  :boolean
439
469
  when /\A(real|float|double( precision)?)\z/io
440
470
  :float
@@ -21,7 +21,7 @@ module Sequel
21
21
  UNSIGNED = ' UNSIGNED'.freeze
22
22
 
23
23
  # The order of column modifiers to use when defining a column.
24
- COLUMN_DEFINITION_ORDER = [:default, :null, :unique, :primary_key, :auto_increment, :references]
24
+ COLUMN_DEFINITION_ORDER = [:collate, :default, :null, :unique, :primary_key, :auto_increment, :references]
25
25
 
26
26
  # Adds a column to the specified table. This method expects a column name,
27
27
  # a datatype and optionally a hash with additional constraints and options:
@@ -111,7 +111,11 @@ module Sequel
111
111
 
112
112
  # Creates the table unless the table already exists
113
113
  def create_table?(name, options={}, &block)
114
- create_table(name, options, &block) unless table_exists?(name)
114
+ if supports_create_table_if_not_exists?
115
+ create_table(name, options.merge(:if_not_exists=>true), &block)
116
+ elsif !table_exists?(name)
117
+ create_table(name, options, &block)
118
+ end
115
119
  end
116
120
 
117
121
  # Creates a view, replacing it if it already exists:
@@ -275,7 +279,12 @@ module Sequel
275
279
  def column_definition_auto_increment_sql(sql, column)
276
280
  sql << " #{auto_increment_sql}" if column[:auto_increment]
277
281
  end
278
-
282
+
283
+ # Add collate SQL fragment to column creation SQL.
284
+ def column_definition_collate_sql(sql, column)
285
+ sql << " COLLATE #{column[:collate]}" if column[:collate]
286
+ end
287
+
279
288
  # Add default SQL fragment to column creation SQL.
280
289
  def column_definition_default_sql(sql, column)
281
290
  sql << " DEFAULT #{literal(column[:default])}" if column.include?(:default)
@@ -366,7 +375,7 @@ module Sequel
366
375
 
367
376
  # DDL statement for creating a table with the given name, columns, and options
368
377
  def create_table_sql(name, generator, options)
369
- "CREATE #{temporary_table_sql if options[:temp]}TABLE #{quote_schema_table(name)} (#{column_list_sql(generator)})"
378
+ "CREATE #{temporary_table_sql if options[:temp]}TABLE#{' IF NOT EXISTS' if options[:if_not_exists]} #{options[:temp] ? quote_identifier(name) : quote_schema_table(name)} (#{column_list_sql(generator)})"
370
379
  end
371
380
 
372
381
  # Default index name for the table and columns, may be too long
@@ -37,6 +37,12 @@ module Sequel
37
37
  false
38
38
  end
39
39
 
40
+ # Whether this dataset supports the +insert_select+ method for returning all columns values
41
+ # directly from an insert query.
42
+ def supports_insert_select?
43
+ false
44
+ end
45
+
40
46
  # Whether the dataset supports the INTERSECT and EXCEPT compound operations, true by default.
41
47
  def supports_intersect_except?
42
48
  true
@@ -81,6 +81,8 @@ module Sequel
81
81
  select_sql
82
82
  when :first
83
83
  clone(:limit=>1).select_sql
84
+ when :insert_select
85
+ clone(:returning=>nil).insert_sql(*@prepared_modify_values)
84
86
  when :insert
85
87
  insert_sql(*@prepared_modify_values)
86
88
  when :update
@@ -95,8 +97,8 @@ module Sequel
95
97
  # and they are substituted using prepared_arg.
96
98
  def literal_symbol(v)
97
99
  if @opts[:bind_vars] and match = PLACEHOLDER_RE.match(v.to_s)
98
- v2 = prepared_arg(match[1].to_sym)
99
- v2 ? literal(v2) : v
100
+ s = match[1].to_sym
101
+ prepared_arg?(s) ? literal(prepared_arg(s)) : v
100
102
  else
101
103
  super
102
104
  end
@@ -118,6 +120,9 @@ module Sequel
118
120
  case @prepared_type
119
121
  when :select, :all
120
122
  all(&block)
123
+ when :insert_select
124
+ meta_def(:select_sql){prepared_sql}
125
+ first
121
126
  when :first
122
127
  first
123
128
  when :insert
@@ -136,6 +141,11 @@ module Sequel
136
141
  @opts[:bind_vars][k]
137
142
  end
138
143
 
144
+ # Whether there is a bound value for the given key.
145
+ def prepared_arg?(k)
146
+ @opts[:bind_vars].has_key?(k)
147
+ end
148
+
139
149
  # Use a clone of the dataset extended with prepared statement
140
150
  # support and using the same argument hash so that you can use
141
151
  # bind variables/prepared arguments in subselects.
@@ -171,6 +181,11 @@ module Sequel
171
181
  prepared_args << k
172
182
  prepared_arg_placeholder
173
183
  end
184
+
185
+ # Always assume there is a prepared arg in the argument mapper.
186
+ def prepared_arg?(k)
187
+ true
188
+ end
174
189
  end
175
190
 
176
191
  # Set the bind variables to use for the call. If bind variables have
@@ -431,6 +431,7 @@ module Sequel
431
431
  v = qualified_column_name(v, last_alias) if v.is_a?(Symbol)
432
432
  [k,v]
433
433
  end
434
+ expr = SQL::BooleanExpression.from_value_pairs(expr)
434
435
  end
435
436
  if block
436
437
  expr2 = yield(table_name, last_alias, @opts[:join] || [])
@@ -702,6 +703,22 @@ module Sequel
702
703
  clone(:overrides=>hash.merge(@opts[:overrides]||{}))
703
704
  end
704
705
 
706
+ # Unbind bound variables from this dataset's filter and return an array of two
707
+ # objects. The first object is a modified dataset where the filter has been
708
+ # replaced with one that uses bound variable placeholders. The second object
709
+ # is the hash of unbound variables. You can then prepare and execute (or just
710
+ # call) the dataset with the bound variables to get results.
711
+ #
712
+ # ds, bv = DB[:items].filter(:a=>1).unbind
713
+ # ds # SELECT * FROM items WHERE (a = $a)
714
+ # bv # {:a => 1}
715
+ # ds.call(:select, bv)
716
+ def unbind
717
+ u = Unbinder.new
718
+ ds = clone(:where=>u.transform(opts[:where]), :join=>u.transform(opts[:join]))
719
+ [ds, u.binds]
720
+ end
721
+
705
722
  # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
706
723
  #
707
724
  # DB[:items].group(:a).having(:a=>1).where(:b).unfiltered
@@ -269,6 +269,7 @@ module Sequel
269
269
  literal(op == :IN ? expr : ~expr)
270
270
  else
271
271
  old_vals = vals
272
+ vals = vals.naked if vals.is_a?(Sequel::Dataset)
272
273
  vals = vals.to_a
273
274
  val_cols = old_vals.columns
274
275
  complex_expression_sql(op, [cols, vals.map!{|x| x.values_at(*val_cols)}])
@@ -774,59 +775,7 @@ module Sequel
774
775
 
775
776
  # Qualify the given expression e to the given table.
776
777
  def qualified_expression(e, table)
777
- case e
778
- when Symbol
779
- t, column, aliaz = split_symbol(e)
780
- if t
781
- e
782
- elsif aliaz
783
- SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(table, SQL::Identifier.new(column)), aliaz)
784
- else
785
- SQL::QualifiedIdentifier.new(table, e)
786
- end
787
- when Array
788
- e.map{|a| qualified_expression(a, table)}
789
- when Hash
790
- h = {}
791
- e.each{|k,v| h[qualified_expression(k, table)] = qualified_expression(v, table)}
792
- h
793
- when SQL::Identifier
794
- SQL::QualifiedIdentifier.new(table, e)
795
- when SQL::OrderedExpression
796
- SQL::OrderedExpression.new(qualified_expression(e.expression, table), e.descending, :nulls=>e.nulls)
797
- when SQL::AliasedExpression
798
- SQL::AliasedExpression.new(qualified_expression(e.expression, table), e.aliaz)
799
- when SQL::CaseExpression
800
- args = [qualified_expression(e.conditions, table), qualified_expression(e.default, table)]
801
- args << qualified_expression(e.expression, table) if e.expression?
802
- SQL::CaseExpression.new(*args)
803
- when SQL::Cast
804
- SQL::Cast.new(qualified_expression(e.expr, table), e.type)
805
- when SQL::Function
806
- SQL::Function.new(e.f, *qualified_expression(e.args, table))
807
- when SQL::ComplexExpression
808
- SQL::ComplexExpression.new(e.op, *qualified_expression(e.args, table))
809
- when SQL::Subscript
810
- SQL::Subscript.new(qualified_expression(e.f, table), qualified_expression(e.sub, table))
811
- when SQL::WindowFunction
812
- SQL::WindowFunction.new(qualified_expression(e.function, table), qualified_expression(e.window, table))
813
- when SQL::Window
814
- o = e.opts.dup
815
- o[:partition] = qualified_expression(o[:partition], table) if o[:partition]
816
- o[:order] = qualified_expression(o[:order], table) if o[:order]
817
- SQL::Window.new(o)
818
- when SQL::PlaceholderLiteralString
819
- args = if e.args.is_a?(Hash)
820
- h = {}
821
- e.args.each{|k,v| h[k] = qualified_expression(v, table)}
822
- h
823
- else
824
- qualified_expression(e.args, table)
825
- end
826
- SQL::PlaceholderLiteralString.new(e.str, args, e.parens)
827
- else
828
- e
829
- end
778
+ Qualifier.new(self, table).transform(e)
830
779
  end
831
780
 
832
781
  # The order of methods to call to build the SELECT SQL statement
@@ -44,6 +44,10 @@ module Sequel
44
44
  # and won't reraise it.
45
45
  class Rollback < Error ; end
46
46
 
47
+ # Exception that occurs when unbinding a dataset that has multiple different values
48
+ # for a given variable.
49
+ class UnbindDuplicate < Error; end
50
+
47
51
  class Error
48
52
  AdapterNotFound = Sequel::AdapterNotFound
49
53
  InvalidOperation = Sequel::InvalidOperation
@@ -0,0 +1,61 @@
1
+ # The columns_introspection extension attempts to introspect the
2
+ # selected columns for a dataset before issuing a query. If it
3
+ # thinks it can guess correctly at the columns the query will use,
4
+ # it will return the columns without issuing a database query.
5
+ # This method is not fool-proof, it's possible that some databases
6
+ # will use column names that Sequel does not expect.
7
+ #
8
+ # To enable this for a single dataset, extend the dataset with
9
+ # Sequel::ColumnIntrospection. To enable this for all datasets, run:
10
+ #
11
+ # Sequel::Dataset.introspect_all_columns
12
+
13
+ module Sequel
14
+ module ColumnsIntrospection
15
+ # Attempt to guess the columns that will be returned
16
+ # if there are columns selected, in order to skip a database
17
+ # query to retrieve the columns. This should work with
18
+ # Symbols, SQL::Identifiers, SQL::QualifiedIdentifiers, and
19
+ # SQL::AliasedExpressions.
20
+ def columns
21
+ return @columns if @columns
22
+ return columns_without_introspection unless cols = opts[:select] and !cols.empty?
23
+ probable_columns = cols.map{|c| probable_column_name(c)}
24
+ if probable_columns.all?
25
+ @columns = probable_columns
26
+ else
27
+ columns_without_introspection
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Return the probable name of the column, or nil if one
34
+ # cannot be determined.
35
+ def probable_column_name(c)
36
+ case c
37
+ when Symbol
38
+ _, c, a = split_symbol(c)
39
+ (a || c).to_sym
40
+ when SQL::Identifier
41
+ c.value.to_sym
42
+ when SQL::QualifiedIdentifier
43
+ col = c.column
44
+ col.is_a?(SQL::Identifier) ? col.value.to_sym : col.to_sym
45
+ when SQL::AliasedExpression
46
+ a = c.aliaz
47
+ a.is_a?(SQL::Identifier) ? a.value.to_sym : a.to_sym
48
+ end
49
+ end
50
+ end
51
+
52
+ class Dataset
53
+ alias columns_without_introspection columns
54
+
55
+ # Enable column introspection for every dataset.
56
+ def self.introspect_all_columns
57
+ include ColumnsIntrospection
58
+ remove_method(:columns) if instance_methods(false).map{|x| x.to_s}.include?('columns')
59
+ end
60
+ end
61
+ end
@@ -302,7 +302,7 @@ module Sequel
302
302
  #
303
303
  # Part of the +migration+ extension.
304
304
  class Migrator
305
- MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z/i.freeze
305
+ MIGRATION_FILE_PATTERN = /\A(\d+)_.+\.rb\z/i.freeze
306
306
  MIGRATION_SPLITTER = '_'.freeze
307
307
  MINIMUM_TIMESTAMP = 20000101
308
308
 
@@ -375,7 +375,8 @@ module Sequel
375
375
  @db = db
376
376
  @directory = directory
377
377
  @files = get_migration_files
378
- @table = opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE)
378
+ schema, table = @db.send(:schema_and_table, opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE))
379
+ @table = schema ? Sequel::SQL::QualifiedIdentifier.new(schema, table) : table
379
380
  @column = opts[:column] || self.class.const_get(:DEFAULT_SCHEMA_COLUMN)
380
381
  @ds = schema_dataset
381
382
  end
@@ -588,7 +589,7 @@ module Sequel
588
589
  next unless MIGRATION_FILE_PATTERN.match(file)
589
590
  files << File.join(directory, file)
590
591
  end
591
- files.sort
592
+ files.sort_by{|f| MIGRATION_FILE_PATTERN.match(File.basename(f))[1].to_i}
592
593
  end
593
594
 
594
595
  # Returns tuples of migration, filename, and direction