sequel 3.23.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 (76) hide show
  1. data/CHANGELOG +64 -0
  2. data/doc/association_basics.rdoc +43 -5
  3. data/doc/model_hooks.rdoc +64 -27
  4. data/doc/prepared_statements.rdoc +8 -4
  5. data/doc/reflection.rdoc +8 -2
  6. data/doc/release_notes/3.23.0.txt +1 -1
  7. data/doc/release_notes/3.24.0.txt +420 -0
  8. data/lib/sequel/adapters/db2.rb +8 -1
  9. data/lib/sequel/adapters/firebird.rb +25 -9
  10. data/lib/sequel/adapters/informix.rb +4 -19
  11. data/lib/sequel/adapters/jdbc.rb +34 -17
  12. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  13. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  14. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  15. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  16. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  17. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  18. data/lib/sequel/adapters/postgres.rb +30 -1
  19. data/lib/sequel/adapters/shared/access.rb +10 -0
  20. data/lib/sequel/adapters/shared/informix.rb +45 -0
  21. data/lib/sequel/adapters/shared/mssql.rb +82 -8
  22. data/lib/sequel/adapters/shared/mysql.rb +25 -7
  23. data/lib/sequel/adapters/shared/postgres.rb +39 -6
  24. data/lib/sequel/adapters/shared/sqlite.rb +57 -5
  25. data/lib/sequel/adapters/sqlite.rb +8 -3
  26. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  27. data/lib/sequel/ast_transformer.rb +190 -0
  28. data/lib/sequel/core.rb +1 -1
  29. data/lib/sequel/database/misc.rb +6 -0
  30. data/lib/sequel/database/query.rb +33 -3
  31. data/lib/sequel/database/schema_methods.rb +6 -2
  32. data/lib/sequel/dataset/features.rb +6 -0
  33. data/lib/sequel/dataset/prepared_statements.rb +17 -2
  34. data/lib/sequel/dataset/query.rb +17 -0
  35. data/lib/sequel/dataset/sql.rb +2 -53
  36. data/lib/sequel/exceptions.rb +4 -0
  37. data/lib/sequel/extensions/to_dot.rb +95 -83
  38. data/lib/sequel/model.rb +5 -0
  39. data/lib/sequel/model/associations.rb +80 -14
  40. data/lib/sequel/model/base.rb +182 -55
  41. data/lib/sequel/model/exceptions.rb +3 -1
  42. data/lib/sequel/plugins/association_pks.rb +6 -4
  43. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  44. data/lib/sequel/plugins/many_through_many.rb +8 -3
  45. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  46. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  47. data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
  48. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  49. data/lib/sequel/sql.rb +8 -0
  50. data/lib/sequel/version.rb +1 -1
  51. data/spec/adapters/postgres_spec.rb +43 -18
  52. data/spec/core/connection_pool_spec.rb +56 -77
  53. data/spec/core/database_spec.rb +25 -0
  54. data/spec/core/dataset_spec.rb +127 -16
  55. data/spec/core/expression_filters_spec.rb +13 -0
  56. data/spec/core/schema_spec.rb +6 -1
  57. data/spec/extensions/association_pks_spec.rb +7 -0
  58. data/spec/extensions/defaults_setter_spec.rb +64 -0
  59. data/spec/extensions/many_through_many_spec.rb +60 -4
  60. data/spec/extensions/nested_attributes_spec.rb +1 -0
  61. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  62. data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
  63. data/spec/extensions/prepared_statements_spec.rb +72 -0
  64. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  65. data/spec/extensions/to_dot_spec.rb +3 -5
  66. data/spec/integration/associations_test.rb +155 -1
  67. data/spec/integration/dataset_test.rb +8 -1
  68. data/spec/integration/plugin_test.rb +119 -0
  69. data/spec/integration/prepared_statement_test.rb +72 -1
  70. data/spec/integration/schema_test.rb +66 -8
  71. data/spec/integration/transaction_test.rb +40 -0
  72. data/spec/model/associations_spec.rb +349 -8
  73. data/spec/model/base_spec.rb +59 -0
  74. data/spec/model/hooks_spec.rb +161 -0
  75. data/spec/model/record_spec.rb +24 -0
  76. metadata +21 -4
@@ -173,7 +173,7 @@ module Sequel
173
173
  end
174
174
  end
175
175
  unless cps
176
- cps = conn.prepare(sql)
176
+ cps = log_yield("Preparing #{name}: #{sql}"){conn.prepare(sql)}
177
177
  conn.prepared_statements[name] = [cps, sql]
178
178
  end
179
179
  if block
@@ -217,7 +217,7 @@ module Sequel
217
217
  # but with the keys converted to strings.
218
218
  def map_to_prepared_args(hash)
219
219
  args = {}
220
- hash.each{|k,v| args[k.to_s] = v}
220
+ hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
221
221
  args
222
222
  end
223
223
 
@@ -226,7 +226,12 @@ module Sequel
226
226
  # SQLite uses a : before the name of the argument for named
227
227
  # arguments.
228
228
  def prepared_arg(k)
229
- LiteralString.new("#{prepared_arg_placeholder}#{k}")
229
+ LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
230
+ end
231
+
232
+ # Always assume a prepared argument.
233
+ def prepared_arg?(k)
234
+ true
230
235
  end
231
236
  end
232
237
 
@@ -25,6 +25,15 @@ module Sequel
25
25
  def schema_column_type(db_type)
26
26
  db_type == 'tinyint(1)' ? :boolean : super
27
27
  end
28
+
29
+ # By default, MySQL 'where id is null' selects the last inserted id.
30
+ # Turn that off unless explicitly enabled.
31
+ def setup_connection(conn)
32
+ super
33
+ sql = "SET SQL_AUTO_IS_NULL=0"
34
+ log_yield(sql){conn.execute(sql)} unless opts[:auto_is_null]
35
+ conn
36
+ end
28
37
  end
29
38
 
30
39
  # Dataset class for MySQL datasets accessed via Swift.
@@ -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
@@ -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
@@ -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:
@@ -371,7 +375,7 @@ module Sequel
371
375
 
372
376
  # DDL statement for creating a table with the given name, columns, and options
373
377
  def create_table_sql(name, generator, options)
374
- "CREATE #{temporary_table_sql if options[:temp]}TABLE #{options[:temp] ? quote_identifier(name) : 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)})"
375
379
  end
376
380
 
377
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