sequel 3.23.0 → 3.24.0

Sign up to get free protection for your applications and to get access to all the features.
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