sequel 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +1 -1
  3. data/doc/cheat_sheet.rdoc +5 -2
  4. data/doc/opening_databases.rdoc +2 -0
  5. data/doc/release_notes/3.5.0.txt +510 -0
  6. data/lib/sequel/adapters/ado.rb +3 -1
  7. data/lib/sequel/adapters/ado/mssql.rb +2 -2
  8. data/lib/sequel/adapters/do.rb +2 -11
  9. data/lib/sequel/adapters/do/mysql.rb +7 -0
  10. data/lib/sequel/adapters/do/postgres.rb +2 -2
  11. data/lib/sequel/adapters/firebird.rb +3 -3
  12. data/lib/sequel/adapters/informix.rb +3 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +3 -3
  14. data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
  15. data/lib/sequel/adapters/mysql.rb +60 -21
  16. data/lib/sequel/adapters/odbc.rb +1 -1
  17. data/lib/sequel/adapters/openbase.rb +3 -3
  18. data/lib/sequel/adapters/oracle.rb +1 -5
  19. data/lib/sequel/adapters/postgres.rb +3 -3
  20. data/lib/sequel/adapters/shared/mssql.rb +142 -33
  21. data/lib/sequel/adapters/shared/mysql.rb +54 -31
  22. data/lib/sequel/adapters/shared/oracle.rb +17 -6
  23. data/lib/sequel/adapters/shared/postgres.rb +7 -7
  24. data/lib/sequel/adapters/shared/progress.rb +3 -3
  25. data/lib/sequel/adapters/shared/sqlite.rb +3 -17
  26. data/lib/sequel/connection_pool.rb +4 -6
  27. data/lib/sequel/core.rb +29 -113
  28. data/lib/sequel/database.rb +14 -12
  29. data/lib/sequel/dataset.rb +8 -21
  30. data/lib/sequel/dataset/convenience.rb +1 -1
  31. data/lib/sequel/dataset/graph.rb +9 -2
  32. data/lib/sequel/dataset/sql.rb +170 -104
  33. data/lib/sequel/exceptions.rb +3 -0
  34. data/lib/sequel/extensions/looser_typecasting.rb +21 -0
  35. data/lib/sequel/extensions/named_timezones.rb +61 -0
  36. data/lib/sequel/extensions/schema_dumper.rb +7 -1
  37. data/lib/sequel/extensions/sql_expr.rb +122 -0
  38. data/lib/sequel/extensions/string_date_time.rb +4 -4
  39. data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
  40. data/lib/sequel/model/associations.rb +105 -45
  41. data/lib/sequel/model/base.rb +37 -28
  42. data/lib/sequel/plugins/active_model.rb +35 -0
  43. data/lib/sequel/plugins/association_dependencies.rb +96 -0
  44. data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
  45. data/lib/sequel/plugins/force_encoding.rb +61 -0
  46. data/lib/sequel/plugins/many_through_many.rb +32 -11
  47. data/lib/sequel/plugins/nested_attributes.rb +7 -2
  48. data/lib/sequel/plugins/subclasses.rb +45 -0
  49. data/lib/sequel/plugins/touch.rb +118 -0
  50. data/lib/sequel/plugins/typecast_on_load.rb +61 -0
  51. data/lib/sequel/sql.rb +31 -30
  52. data/lib/sequel/timezones.rb +161 -0
  53. data/lib/sequel/version.rb +1 -1
  54. data/spec/adapters/mssql_spec.rb +262 -0
  55. data/spec/adapters/mysql_spec.rb +46 -8
  56. data/spec/adapters/postgres_spec.rb +6 -3
  57. data/spec/adapters/spec_helper.rb +21 -0
  58. data/spec/adapters/sqlite_spec.rb +1 -1
  59. data/spec/core/connection_pool_spec.rb +1 -1
  60. data/spec/core/database_spec.rb +27 -1
  61. data/spec/core/dataset_spec.rb +63 -1
  62. data/spec/core/object_graph_spec.rb +1 -1
  63. data/spec/core/schema_spec.rb +1 -0
  64. data/spec/extensions/active_model_spec.rb +47 -0
  65. data/spec/extensions/association_dependencies_spec.rb +108 -0
  66. data/spec/extensions/class_table_inheritance_spec.rb +252 -0
  67. data/spec/extensions/force_encoding_spec.rb +75 -0
  68. data/spec/extensions/looser_typecasting_spec.rb +39 -0
  69. data/spec/extensions/many_through_many_spec.rb +60 -2
  70. data/spec/extensions/named_timezones_spec.rb +72 -0
  71. data/spec/extensions/nested_attributes_spec.rb +29 -1
  72. data/spec/extensions/schema_dumper_spec.rb +10 -0
  73. data/spec/extensions/spec_helper.rb +1 -1
  74. data/spec/extensions/sql_expr_spec.rb +89 -0
  75. data/spec/extensions/subclasses_spec.rb +52 -0
  76. data/spec/extensions/thread_local_timezones_spec.rb +45 -0
  77. data/spec/extensions/touch_spec.rb +155 -0
  78. data/spec/extensions/typecast_on_load_spec.rb +60 -0
  79. data/spec/integration/database_test.rb +8 -0
  80. data/spec/integration/dataset_test.rb +9 -9
  81. data/spec/integration/plugin_test.rb +139 -0
  82. data/spec/integration/schema_test.rb +7 -7
  83. data/spec/integration/spec_helper.rb +32 -1
  84. data/spec/integration/timezone_test.rb +3 -3
  85. data/spec/integration/transaction_test.rb +1 -1
  86. data/spec/integration/type_test.rb +6 -6
  87. data/spec/model/association_reflection_spec.rb +18 -0
  88. data/spec/model/associations_spec.rb +169 -9
  89. data/spec/model/base_spec.rb +2 -0
  90. data/spec/model/eager_loading_spec.rb +82 -2
  91. data/spec/model/model_spec.rb +8 -1
  92. data/spec/model/record_spec.rb +52 -9
  93. metadata +33 -23
@@ -31,6 +31,7 @@ module Sequel
31
31
  TRANSACTION_ROLLBACK = 'Transaction.rollback'.freeze
32
32
 
33
33
  POSTGRES_DEFAULT_RE = /\A(?:B?('.*')::[^']+|\((-?\d+(?:\.\d+)?)\))\z/
34
+ MSSQL_DEFAULT_RE = /\A(?:\(N?('.*')\)|\(\((-?\d+(?:\.\d+)?)\)\))\z/
34
35
  MYSQL_TIMESTAMP_RE = /\ACURRENT_(?:DATE|TIMESTAMP)?\z/
35
36
  STRING_DEFAULT_RE = /\A'(.*)'\z/
36
37
 
@@ -109,7 +110,7 @@ module Sequel
109
110
  begin
110
111
  Sequel.require "adapters/#{scheme}"
111
112
  rescue LoadError => e
112
- raise AdapterNotFound, "Could not load #{scheme} adapter:\n #{e.message}"
113
+ raise Sequel.convert_exception_class(e, AdapterNotFound)
113
114
  end
114
115
 
115
116
  # make sure we actually loaded the adapter
@@ -533,10 +534,8 @@ module Sequel
533
534
  meth = "typecast_value_#{column_type}"
534
535
  begin
535
536
  respond_to?(meth, true) ? send(meth, value) : value
536
- rescue ArgumentError, TypeError => exp
537
- e = Sequel::InvalidValue.new("#{exp.class} #{exp.message}")
538
- e.set_backtrace(exp.backtrace)
539
- raise e
537
+ rescue ArgumentError, TypeError => e
538
+ raise Sequel.convert_exception_class(e, InvalidValue)
540
539
  end
541
540
  end
542
541
 
@@ -658,10 +657,13 @@ module Sequel
658
657
  if database_type == :postgres and m = POSTGRES_DEFAULT_RE.match(default)
659
658
  default = m[1] || m[2]
660
659
  end
661
- if [:string, :blob, :date, :datetime, :time].include?(type)
660
+ if database_type == :mssql and m = MSSQL_DEFAULT_RE.match(default)
661
+ default = m[1] || m[2]
662
+ end
663
+ if [:string, :blob, :date, :datetime, :time, :enum].include?(type)
662
664
  if database_type == :mysql
663
665
  return if [:date, :datetime, :time].include?(type) && MYSQL_TIMESTAMP_RE.match(default)
664
- orig_default = default = "'#{default.gsub("'", "''").gsub('\\', '\\\\')}'"
666
+ orig_default = default = "'#{default.gsub("'", "''").gsub('\\', '\\\\')}'"
665
667
  end
666
668
  return unless m = STRING_DEFAULT_RE.match(default)
667
669
  default = m[1].gsub("''", "'")
@@ -675,7 +677,7 @@ module Sequel
675
677
  when /[t1]/i
676
678
  true
677
679
  end
678
- when :string
680
+ when :string, :enum
679
681
  default
680
682
  when :blob
681
683
  Sequel::SQL::Blob.new(default)
@@ -801,9 +803,7 @@ module Sequel
801
803
  # and traceback.
802
804
  def raise_error(exception, opts={})
803
805
  if !opts[:classes] || Array(opts[:classes]).any?{|c| exception.is_a?(c)}
804
- e = (opts[:disconnect] ? DatabaseDisconnectError : DatabaseError).new("#{exception.class}: #{exception.message}")
805
- e.set_backtrace(exception.backtrace)
806
- raise e
806
+ raise Sequel.convert_exception_class(exception, opts[:disconnect] ? DatabaseDisconnectError : DatabaseError)
807
807
  else
808
808
  raise exception
809
809
  end
@@ -876,6 +876,8 @@ module Sequel
876
876
  :decimal
877
877
  when /bytea|blob|image|(var)?binary/io
878
878
  :blob
879
+ when /\Aenum/
880
+ :enum
879
881
  end
880
882
  end
881
883
 
@@ -906,7 +908,7 @@ module Sequel
906
908
 
907
909
  # Raise a database error unless the exception is an Rollback.
908
910
  def transaction_error(e)
909
- raise_error(e, :classes=>database_error_classes) unless Rollback === e
911
+ raise_error(e, :classes=>database_error_classes) unless e.is_a?(Rollback)
910
912
  end
911
913
 
912
914
  # Typecast the value to an SQL::Blob
@@ -51,7 +51,7 @@ module Sequel
51
51
  unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym}
52
52
 
53
53
  NOTIMPL_MSG = "This method must be overridden in Sequel adapters".freeze
54
- WITH_SUPPORTED='with'.freeze
54
+ WITH_SUPPORTED=:select_with_sql
55
55
 
56
56
  # The database that corresponds to this dataset
57
57
  attr_accessor :db
@@ -97,7 +97,7 @@ module Sequel
97
97
  # options of the resulting dataset.
98
98
  def self.def_mutation_method(*meths)
99
99
  meths.each do |meth|
100
- class_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end")
100
+ class_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
101
101
  end
102
102
  end
103
103
 
@@ -163,12 +163,12 @@ module Sequel
163
163
  # Add a mutation method to this dataset instance.
164
164
  def def_mutation_method(*meths)
165
165
  meths.each do |meth|
166
- instance_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end")
166
+ instance_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
167
167
  end
168
168
  end
169
169
 
170
170
  # Deletes the records in the dataset. The returned value is generally the
171
- # number of records deleted, but that is adapter dependent.
171
+ # number of records deleted, but that is adapter dependent. See delete_sql.
172
172
  def delete
173
173
  execute_dui(delete_sql)
174
174
  end
@@ -200,6 +200,7 @@ module Sequel
200
200
 
201
201
  # Inserts values into the associated table. The returned value is generally
202
202
  # the value of the primary key for the inserted row, but that is adapter dependent.
203
+ # See insert_sql.
203
204
  def insert(*values)
204
205
  execute_insert(insert_sql(*values))
205
206
  end
@@ -242,7 +243,7 @@ module Sequel
242
243
  update(*args)
243
244
  end
244
245
 
245
- # Set the default values for insert and update statements. The values passed
246
+ # Set the default values for insert and update statements. The values hash passed
246
247
  # to insert or update are merged into this hash.
247
248
  def set_defaults(hash)
248
249
  clone(:defaults=>(@opts[:defaults]||{}).merge(hash))
@@ -256,7 +257,7 @@ module Sequel
256
257
 
257
258
  # Whether the dataset supports common table expressions (the WITH clause).
258
259
  def supports_cte?
259
- select_clause_order.include?(WITH_SUPPORTED)
260
+ select_clause_methods.include?(WITH_SUPPORTED)
260
261
  end
261
262
 
262
263
  # Whether the dataset supports the DISTINCT ON clause, true by default.
@@ -300,7 +301,7 @@ module Sequel
300
301
  end
301
302
 
302
303
  # Updates values for the dataset. The returned value is generally the
303
- # number of rows updated, but that is adapter dependent.
304
+ # number of rows updated, but that is adapter dependent. See update_sql.
304
305
  def update(values={})
305
306
  execute_dui(update_sql(values))
306
307
  end
@@ -377,19 +378,5 @@ module Sequel
377
378
  # default, added to make the model eager loading code simpler.
378
379
  def post_load(all_records)
379
380
  end
380
-
381
- # If a block argument is passed to a method that uses a VirtualRow,
382
- # yield a new VirtualRow instance to the block if it accepts a single
383
- # argument. Otherwise, evaluate the block in the context of a new
384
- # VirtualRow instance.
385
- def virtual_row_block_call(block)
386
- return unless block
387
- case block.arity
388
- when -1, 0
389
- SQL::VirtualRow.new.instance_eval(&block)
390
- else
391
- block.call(SQL::VirtualRow.new)
392
- end
393
- end
394
381
  end
395
382
  end
@@ -109,7 +109,7 @@ module Sequel
109
109
  # # this will commit every 50 records
110
110
  # dataset.import([:x, :y], [[1, 2], [3, 4], ...], :slice => 50)
111
111
  def import(columns, values, opts={})
112
- return @db.transaction{execute_dui("#{insert_sql_base}#{quote_schema_table(@opts[:from].first)} (#{identifier_list(columns)}) #{subselect_sql(values)}")} if values.is_a?(Dataset)
112
+ return @db.transaction{insert(columns, values)} if values.is_a?(Dataset)
113
113
 
114
114
  return if values.empty?
115
115
  raise(Error, IMPORT_ERROR_MSG) if columns.empty?
@@ -41,6 +41,10 @@ module Sequel
41
41
  # or an object that responds to .dataset and return a symbol or a dataset
42
42
  # * join_conditions - Any condition(s) allowed by join_table.
43
43
  # * options - A hash of graph options. The following options are currently used:
44
+ # * :from_self_alias - The alias to use when the receiver is not a graphed
45
+ # dataset but it contains multiple FROM tables or a JOIN. In this case,
46
+ # the receiver is wrapped in a from_self before graphing, and this option
47
+ # determines the alias to use.
44
48
  # * :implicit_qualifier - The qualifier of implicit conditions, see #join_table.
45
49
  # * :join_type - The type of join to use (passed to join_table). Defaults to
46
50
  # :left_outer.
@@ -83,9 +87,12 @@ module Sequel
83
87
 
84
88
  # Only allow table aliases that haven't been used
85
89
  raise_alias_error.call if @opts[:graph] && @opts[:graph][:table_aliases] && @opts[:graph][:table_aliases].include?(table_alias)
86
-
90
+
91
+ # Use a from_self if this is already a joined table
92
+ ds = (!@opts[:graph] && (@opts[:from].length > 1 || @opts[:join])) ? from_self(:alias=>options[:from_self_alias] || first_source) : self
93
+
87
94
  # Join the table early in order to avoid cloning the dataset twice
88
- ds = join_table(options[:join_type] || :left_outer, table, join_conditions, :table_alias=>table_alias, :implicit_qualifier=>options[:implicit_qualifier], &block)
95
+ ds = ds.join_table(options[:join_type] || :left_outer, table, join_conditions, :table_alias=>table_alias, :implicit_qualifier=>options[:implicit_qualifier], &block)
89
96
  opts = ds.opts
90
97
 
91
98
  # Whether to include the table in the result set
@@ -1,5 +1,12 @@
1
1
  module Sequel
2
2
  class Dataset
3
+
4
+ # Given a type (e.g. select) and an array of clauses,
5
+ # return an array of methods to call to build the SQL string.
6
+ def self.clause_methods(type, clauses)
7
+ clauses.map{|clause| :"#{type}_#{clause}_sql"}.freeze
8
+ end
9
+
3
10
  AND_SEPARATOR = " AND ".freeze
4
11
  BOOL_FALSE = "'f'".freeze
5
12
  BOOL_TRUE = "'t'".freeze
@@ -8,7 +15,7 @@ module Sequel
8
15
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
9
16
  COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
10
17
  DATASET_ALIAS_BASE_NAME = 't'.freeze
11
- INSERT_SQL_BASE="INSERT INTO ".freeze
18
+ FROM_SELF_KEEP_OPTS = [:graph, :eager_graph, :graph_aliases]
12
19
  IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
13
20
  IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
14
21
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
@@ -16,7 +23,10 @@ module Sequel
16
23
  QUALIFY_KEYS = [:select, :where, :having, :order, :group]
17
24
  QUESTION_MARK = '?'.freeze
18
25
  STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
19
- SELECT_CLAUSE_ORDER = %w'with distinct columns from join where group having compounds order limit'.freeze
26
+ DELETE_CLAUSE_METHODS = clause_methods(:delete, %w'from where')
27
+ INSERT_CLAUSE_METHODS = clause_methods(:insert, %w'into columns values')
28
+ SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct columns from join where group having compounds order limit')
29
+ UPDATE_CLAUSE_METHODS = clause_methods(:update, %w'table set where')
20
30
  TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S%N%z'".freeze
21
31
  STANDARD_TIMESTAMP_FORMAT = "TIMESTAMP #{TIMESTAMP_FORMAT}".freeze
22
32
  TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
@@ -106,19 +116,9 @@ module Sequel
106
116
  # dataset.filter{|o| o.price >= 100}.delete_sql #=>
107
117
  # "DELETE FROM items WHERE (price >= 100)"
108
118
  def delete_sql
109
- opts = @opts
110
-
111
119
  return static_sql(opts[:sql]) if opts[:sql]
112
-
113
120
  check_modification_allowed!
114
-
115
- sql = "DELETE FROM #{source_list(opts[:from])}"
116
-
117
- if where = opts[:where]
118
- sql << " WHERE #{literal(where)}"
119
- end
120
-
121
- sql
121
+ clause_sql(:delete)
122
122
  end
123
123
 
124
124
  # Returns a copy of the dataset with the SQL DISTINCT clause.
@@ -168,7 +168,7 @@ module Sequel
168
168
  # Returns an EXISTS clause for the dataset as a LiteralString.
169
169
  #
170
170
  # DB.select(1).where(DB[:items].exists).sql
171
- # #=> "SELECT 1 WHERE EXISTS (SELECT * FROM items)"
171
+ # #=> "SELECT 1 WHERE (EXISTS (SELECT * FROM items))"
172
172
  def exists
173
173
  LiteralString.new("EXISTS (#{select_sql})")
174
174
  end
@@ -283,7 +283,7 @@ module Sequel
283
283
  # ds.from_self(:alias=>:foo).sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 'foo'"
284
284
  def from_self(opts={})
285
285
  fs = {}
286
- @opts.keys.each{|k| fs[k] = nil}
286
+ @opts.keys.each{|k| fs[k] = nil unless FROM_SELF_KEEP_OPTS.include?(k)}
287
287
  clone(fs).from(opts[:alias] ? as(opts[:alias]) : self)
288
288
  end
289
289
 
@@ -336,57 +336,60 @@ module Sequel
336
336
  end
337
337
  end
338
338
 
339
- # Formats an INSERT statement using the given values. If a hash is given,
340
- # the resulting statement includes column names. If no values are given,
341
- # the resulting statement includes a DEFAULT VALUES clause.
339
+ # Formats an INSERT statement using the given values. The API is a little
340
+ # complex, and best explained by example:
342
341
  #
343
- # dataset.insert_sql #=> 'INSERT INTO items DEFAULT VALUES'
344
- # dataset.insert_sql(1,2,3) #=> 'INSERT INTO items VALUES (1, 2, 3)'
345
- # dataset.insert_sql(:a => 1, :b => 2) #=>
346
- # 'INSERT INTO items (a, b) VALUES (1, 2)'
342
+ # # Default values
343
+ # DB[:items].insert_sql #=> 'INSERT INTO items DEFAULT VALUES'
344
+ # DB[:items].insert_sql({}) #=> 'INSERT INTO items DEFAULT VALUES'
345
+ # # Values without columns
346
+ # DB[:items].insert_sql(1,2,3) #=> 'INSERT INTO items VALUES (1, 2, 3)'
347
+ # DB[:items].insert_sql([1,2,3]) #=> 'INSERT INTO items VALUES (1, 2, 3)'
348
+ # # Values with columns
349
+ # DB[:items].insert_sql([:a, :b], [1,2]) #=> 'INSERT INTO items (a, b) VALUES (1, 2)'
350
+ # DB[:items].insert_sql(:a => 1, :b => 2) #=> 'INSERT INTO items (a, b) VALUES (1, 2)'
351
+ # # Using a subselect
352
+ # DB[:items].insert_sql(DB[:old_items]) #=> 'INSERT INTO items SELECT * FROM old_items
353
+ # # Using a subselect with columns
354
+ # DB[:items].insert_sql([:a, :b], DB[:old_items]) #=> 'INSERT INTO items (a, b) SELECT * FROM old_items
347
355
  def insert_sql(*values)
348
356
  return static_sql(@opts[:sql]) if @opts[:sql]
349
357
 
350
358
  check_modification_allowed!
351
359
 
352
- from = source_list(@opts[:from])
360
+ columns = []
361
+
353
362
  case values.size
354
363
  when 0
355
- values = {}
364
+ return insert_sql({})
356
365
  when 1
357
- vals = values.at(0)
358
- if [Hash, Dataset, Array].any?{|c| vals.is_a?(c)}
366
+ case vals = values.at(0)
367
+ when Hash
368
+ vals = @opts[:defaults].merge(vals) if @opts[:defaults]
369
+ vals = vals.merge(@opts[:overrides]) if @opts[:overrides]
370
+ values = []
371
+ vals.each do |k,v|
372
+ columns << k
373
+ values << v
374
+ end
375
+ when Dataset, Array, LiteralString
359
376
  values = vals
360
- elsif vals.respond_to?(:values)
361
- values = vals.values
362
- end
363
- end
364
-
365
- case values
366
- when Array
367
- if values.empty?
368
- insert_default_values_sql
369
377
  else
370
- "#{insert_sql_base}#{from} VALUES #{literal(values)}#{insert_sql_suffix}"
371
- end
372
- when Hash
373
- values = @opts[:defaults].merge(values) if @opts[:defaults]
374
- values = values.merge(@opts[:overrides]) if @opts[:overrides]
375
- if values.empty?
376
- insert_default_values_sql
377
- else
378
- fl, vl = [], []
379
- values.each do |k, v|
380
- fl << literal(String === k ? k.to_sym : k)
381
- vl << literal(v)
378
+ if vals.respond_to?(:values) && (v = vals.values).is_a?(Hash)
379
+ return insert_sql(v)
382
380
  end
383
- "#{insert_sql_base}#{from} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})#{insert_sql_suffix}"
384
381
  end
385
- when Dataset
386
- "#{insert_sql_base}#{from} #{literal(values)}#{insert_sql_suffix}"
382
+ when 2
383
+ if (v0 = values.at(0)).is_a?(Array) && ((v1 = values.at(1)).is_a?(Array) || v1.is_a?(Dataset) || v1.is_a?(LiteralString))
384
+ columns, values = v0, v1
385
+ raise(Error, "Different number of values and columns given to insert_sql") if values.is_a?(Array) and columns.length != values.length
386
+ end
387
387
  end
388
+
389
+ columns = columns.map{|k| literal(String === k ? k.to_sym : k)}
390
+ clone(:columns=>columns, :values=>values)._insert_sql
388
391
  end
389
-
392
+
390
393
  # Adds an INTERSECT clause using a second dataset object.
391
394
  # An INTERSECT compound dataset returns all rows in both the current dataset
392
395
  # and the given dataset.
@@ -590,8 +593,7 @@ module Sequel
590
593
  # This method should be overridden by descendants if the support
591
594
  # inserting multiple records in a single SQL statement.
592
595
  def multi_insert_sql(columns, values)
593
- s = "#{insert_sql_base}#{source_list(@opts[:from])} (#{identifier_list(columns)}) VALUES "
594
- values.map{|r| s + literal(r)}
596
+ values.map{|r| insert_sql(columns, r)}
595
597
  end
596
598
 
597
599
  # Adds an alternate filter to an existing filter using OR. If no filter
@@ -619,7 +621,7 @@ module Sequel
619
621
  # ds.order{|o| o.sum(:name)}.sql #=> 'SELECT * FROM items ORDER BY sum(name)'
620
622
  # ds.order(nil).sql #=> 'SELECT * FROM items'
621
623
  def order(*columns, &block)
622
- columns += Array(virtual_row_block_call(block)) if block
624
+ columns += Array(Sequel.virtual_row(&block)) if block
623
625
  clone(:order => (columns.compact.empty?) ? nil : columns)
624
626
  end
625
627
  alias_method :order_by, :order
@@ -630,7 +632,8 @@ module Sequel
630
632
  # ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
631
633
  # ds.order(:a).order_more(:b).sql #=> 'SELECT * FROM items ORDER BY a, b'
632
634
  def order_more(*columns, &block)
633
- order(*Array(@opts[:order]).concat(columns), &block)
635
+ columns = @opts[:order] + columns if @opts[:order]
636
+ order(*columns, &block)
634
637
  end
635
638
 
636
639
  # SQL fragment for the ordered expression, used in the ORDER BY
@@ -740,7 +743,7 @@ module Sequel
740
743
  # dataset.select(:a, :b) # SELECT a, b FROM items
741
744
  # dataset.select{|o| o.a, o.sum(:b)} # SELECT a, sum(b) FROM items
742
745
  def select(*columns, &block)
743
- columns += Array(virtual_row_block_call(block)) if block
746
+ columns += Array(Sequel.virtual_row(&block)) if block
744
747
  m = []
745
748
  columns.map do |i|
746
749
  i.is_a?(Hash) ? m.concat(i.map{|k, v| SQL::AliasedExpression.new(k,v)}) : m << i
@@ -761,7 +764,8 @@ module Sequel
761
764
  # dataset.select(:a).select(:b) # SELECT b FROM items
762
765
  # dataset.select(:a).select_more(:b) # SELECT a, b FROM items
763
766
  def select_more(*columns, &block)
764
- select(*Array(@opts[:select]).concat(columns), &block)
767
+ columns = @opts[:select] + columns if @opts[:select]
768
+ select(*columns, &block)
765
769
  end
766
770
 
767
771
  # Formats a SELECT statement
@@ -769,9 +773,7 @@ module Sequel
769
773
  # dataset.select_sql # => "SELECT * FROM items"
770
774
  def select_sql
771
775
  return static_sql(@opts[:sql]) if @opts[:sql]
772
- sql = 'SELECT'
773
- select_clause_order.each{|x| send(:"select_#{x}_sql", sql)}
774
- sql
776
+ clause_sql(:select)
775
777
  end
776
778
 
777
779
  # Same as select_sql, not aliased directly to make subclassing simpler.
@@ -845,32 +847,11 @@ module Sequel
845
847
  # Raises an error if the dataset is grouped or includes more
846
848
  # than one table.
847
849
  def update_sql(values = {})
848
- opts = @opts
849
-
850
850
  return static_sql(opts[:sql]) if opts[:sql]
851
-
852
851
  check_modification_allowed!
853
-
854
- sql = "UPDATE #{source_list(@opts[:from])} SET "
855
- set = if values.is_a?(Hash)
856
- values = opts[:defaults].merge(values) if opts[:defaults]
857
- values = values.merge(opts[:overrides]) if opts[:overrides]
858
- # get values from hash
859
- values.map do |k, v|
860
- "#{[String, Symbol].any?{|c| k.is_a?(c)} ? quote_identifier(k) : literal(k)} = #{literal(v)}"
861
- end.join(COMMA_SEPARATOR)
862
- else
863
- # copy values verbatim
864
- values
865
- end
866
- sql << set
867
- if where = opts[:where]
868
- sql << " WHERE #{literal(where)}"
869
- end
870
-
871
- sql
852
+ clone(:values=>values)._update_sql
872
853
  end
873
-
854
+
874
855
  # Add a condition to the WHERE clause. See #filter for argument types.
875
856
  #
876
857
  # dataset.group(:a).having(:a).filter(:b) # SELECT * FROM items GROUP BY a HAVING a AND b
@@ -933,12 +914,22 @@ module Sequel
933
914
  end
934
915
 
935
916
  [:inner, :full_outer, :right_outer, :left_outer].each do |jtype|
936
- class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end")
917
+ class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end", __FILE__, __LINE__)
937
918
  end
938
919
  alias join inner_join
939
920
 
940
921
  protected
941
922
 
923
+ # Formats in INSERT statement using the stored columns and values.
924
+ def _insert_sql
925
+ clause_sql(:insert)
926
+ end
927
+
928
+ # Formats an UPDATE statement using the stored values.
929
+ def _update_sql
930
+ clause_sql(:update)
931
+ end
932
+
942
933
  # Return a from_self dataset if an order or limit is specified, so it works as expected
943
934
  # with UNION, EXCEPT, and INTERSECT clauses.
944
935
  def compound_from_self
@@ -955,6 +946,12 @@ module Sequel
955
946
  clone(clause => cond)
956
947
  end
957
948
 
949
+ # Formats the truncate statement. Assumes the table given has already been
950
+ # literalized.
951
+ def _truncate_sql(table)
952
+ "TRUNCATE TABLE #{table}"
953
+ end
954
+
958
955
  # Do a simple join of the arguments (which should be strings or symbols) separated by commas
959
956
  def argument_list(args)
960
957
  args.join(COMMA_SEPARATOR)
@@ -972,6 +969,13 @@ module Sequel
972
969
  raise(InvalidOperation, "Joined datasets cannot be modified") if (opts[:from].is_a?(Array) && opts[:from].size > 1) || opts[:join]
973
970
  end
974
971
 
972
+ # Prepare an SQL statement by calling all clause methods for the given statement type.
973
+ def clause_sql(type)
974
+ sql = type.to_s.upcase
975
+ send("#{type}_clause_methods").each{|x| send(x, sql)}
976
+ sql
977
+ end
978
+
975
979
  # Converts an array of column names into a comma seperated string of
976
980
  # column names. If the array is empty, a wildcard (*) is returned.
977
981
  def column_list(columns)
@@ -989,6 +993,11 @@ module Sequel
989
993
  :"#{DATASET_ALIAS_BASE_NAME}#{number}"
990
994
  end
991
995
 
996
+ # The order of methods to call to build the DELETE SQL statement
997
+ def delete_clause_methods
998
+ DELETE_CLAUSE_METHODS
999
+ end
1000
+
992
1001
  # Converts an array of expressions into a comma separated string of
993
1002
  # expressions.
994
1003
  def expression_list(columns)
@@ -1020,7 +1029,7 @@ module Sequel
1020
1029
  SQL::BooleanExpression.new(:AND, *expr.map{|x| filter_expr(x)})
1021
1030
  end
1022
1031
  when Proc
1023
- filter_expr(virtual_row_block_call(expr))
1032
+ filter_expr(Sequel.virtual_row(&expr))
1024
1033
  when SQL::NumericExpression, SQL::StringExpression
1025
1034
  raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
1026
1035
  when Symbol, SQL::Expression
@@ -1042,37 +1051,64 @@ module Sequel
1042
1051
  v2 = Sequel.application_to_database_timestamp(v)
1043
1052
  fmt = default_timestamp_format.gsub(/%[Nz]/) do |m|
1044
1053
  if m == '%N'
1045
- sprintf(".%06d", v.is_a?(DateTime) ? v.sec_fraction*86400000000 : v.usec) if supports_timestamp_usecs?
1054
+ format_timestamp_usec(v.is_a?(DateTime) ? v.sec_fraction*86400000000 : v.usec) if supports_timestamp_usecs?
1046
1055
  else
1047
1056
  if supports_timestamp_timezones?
1048
1057
  # Would like to just use %z format, but it doesn't appear to work on Windows
1049
1058
  # Instead, the offset fragment is constructed manually
1050
1059
  minutes = (v2.is_a?(DateTime) ? v2.offset * 1440 : v2.utc_offset/60).to_i
1051
- sprintf("%+03i%02i", *minutes.divmod(60))
1060
+ format_timestamp_offset(*minutes.divmod(60))
1052
1061
  end
1053
1062
  end
1054
1063
  end
1055
1064
  v2.strftime(fmt)
1056
1065
  end
1066
+
1067
+ # Return the SQL timestamp fragment to use for the timezone offset.
1068
+ def format_timestamp_offset(hour, minute)
1069
+ sprintf("%+03i%02i", hour, minute)
1070
+ end
1057
1071
 
1072
+ # Return the SQL timestamp fragment to use for the fractional time part.
1073
+ # Should start with the decimal point. Uses 6 decimal places by default.
1074
+ def format_timestamp_usec(usec)
1075
+ sprintf(".%06d", usec)
1076
+ end
1077
+
1078
+ # SQL fragment specifying a list of identifiers
1058
1079
  # SQL fragment specifying a list of identifiers
1059
1080
  def identifier_list(columns)
1060
1081
  columns.map{|i| quote_identifier(i)}.join(COMMA_SEPARATOR)
1061
1082
  end
1062
1083
 
1063
- # SQL statement for the beginning of an INSERT statement
1064
- def insert_sql_base
1065
- INSERT_SQL_BASE
1084
+ # SQL fragment specifying the table to insert INTO
1085
+ def insert_into_sql(sql)
1086
+ sql << " INTO #{source_list(@opts[:from])}"
1066
1087
  end
1067
1088
 
1068
- # SQL statement for formatting an insert statement with default values
1069
- def insert_default_values_sql
1070
- "#{insert_sql_base}#{source_list(@opts[:from])} DEFAULT VALUES"
1089
+ # The order of methods to call to build the INSERT SQL statement
1090
+ def insert_clause_methods
1091
+ INSERT_CLAUSE_METHODS
1071
1092
  end
1072
1093
 
1073
- # SQL statement for end of an INSERT statement
1074
- def insert_sql_suffix
1075
- nil
1094
+ # SQL fragment specifying the columns to insert into
1095
+ def insert_columns_sql(sql)
1096
+ columns = opts[:columns]
1097
+ sql << " (#{columns.join(COMMA_SEPARATOR)})" if columns && !columns.empty?
1098
+ end
1099
+
1100
+ # SQL fragment specifying the values to insert.
1101
+ def insert_values_sql(sql)
1102
+ case values = opts[:values]
1103
+ when Array
1104
+ sql << (values.empty? ? " DEFAULT VALUES" : " VALUES #{literal(values)}")
1105
+ when Dataset
1106
+ sql << " #{subselect_sql(values)}"
1107
+ when LiteralString
1108
+ sql << " #{values}"
1109
+ else
1110
+ raise Error, "Unsupported INSERT values type, should be an Array or Dataset: #{values.inspect}"
1111
+ end
1076
1112
  end
1077
1113
 
1078
1114
  # Inverts the given order by breaking it into a list of column references
@@ -1156,7 +1192,9 @@ module Sequel
1156
1192
  v.to_s
1157
1193
  end
1158
1194
 
1159
- # SQL fragmento for a type of object not handled by Dataset#literal. Raises an error. If a database specific type is allowed, this should be overriden in a subclass.
1195
+ # SQL fragmento for a type of object not handled by Dataset#literal.
1196
+ # Raises an error. If a database specific type is allowed,
1197
+ # this should be overriden in a subclass.
1160
1198
  def literal_other(v)
1161
1199
  raise Error, "can't express #{v.inspect} as a SQL literal"
1162
1200
  end
@@ -1262,8 +1300,8 @@ module Sequel
1262
1300
  end
1263
1301
 
1264
1302
  # The order of methods to call to build the SELECT SQL statement
1265
- def select_clause_order
1266
- SELECT_CLAUSE_ORDER
1303
+ def select_clause_methods
1304
+ SELECT_CLAUSE_METHODS
1267
1305
  end
1268
1306
 
1269
1307
  # Modify the sql to add the columns selected
@@ -1293,6 +1331,7 @@ module Sequel
1293
1331
  def select_from_sql(sql)
1294
1332
  sql << " FROM #{source_list(@opts[:from])}" if @opts[:from]
1295
1333
  end
1334
+ alias delete_from_sql select_from_sql
1296
1335
 
1297
1336
  # Modify the sql to add the expressions to GROUP BY
1298
1337
  def select_group_sql(sql)
@@ -1319,18 +1358,24 @@ module Sequel
1319
1358
  def select_order_sql(sql)
1320
1359
  sql << " ORDER BY #{expression_list(@opts[:order])}" if @opts[:order]
1321
1360
  end
1361
+ alias delete_order_sql select_order_sql
1362
+ alias update_order_sql select_order_sql
1322
1363
 
1323
1364
  # Modify the sql to add the filter criteria in the WHERE clause
1324
1365
  def select_where_sql(sql)
1325
1366
  sql << " WHERE #{literal(@opts[:where])}" if @opts[:where]
1326
1367
  end
1368
+ alias delete_where_sql select_where_sql
1369
+ alias update_where_sql select_where_sql
1327
1370
 
1371
+ # SQL Fragment specifying the WITH clause
1328
1372
  def select_with_sql(sql)
1329
1373
  ws = opts[:with]
1330
1374
  return if !ws || ws.empty?
1331
- sql.replace("#{select_with_sql_base}#{ws.map{|w| "#{w[:name]}#{"(#{argument_list(w[:args])})" if w[:args]} AS (#{subselect_sql(w[:dataset])})"}.join(COMMA_SEPARATOR)} #{sql}")
1375
+ sql.replace("#{select_with_sql_base}#{ws.map{|w| "#{quote_identifier(w[:name])}#{"(#{argument_list(w[:args])})" if w[:args]} AS #{literal_dataset(w[:dataset])}"}.join(COMMA_SEPARATOR)} #{sql}")
1332
1376
  end
1333
1377
 
1378
+ # The base keyword to use for the SQL WITH clause
1334
1379
  def select_with_sql_base
1335
1380
  SQL_WITH
1336
1381
  end
@@ -1376,10 +1421,31 @@ module Sequel
1376
1421
  t.is_a?(String) ? quote_identifier(t) : literal(t)
1377
1422
  end
1378
1423
 
1379
- # Formats the truncate statement. Assumes the table given has already been
1380
- # literalized.
1381
- def _truncate_sql(table)
1382
- "TRUNCATE TABLE #{table}"
1424
+ # The order of methods to call to build the UPDATE SQL statement
1425
+ def update_clause_methods
1426
+ UPDATE_CLAUSE_METHODS
1427
+ end
1428
+
1429
+ # SQL fragment specifying the tables from with to delete
1430
+ def update_table_sql(sql)
1431
+ sql << " #{source_list(@opts[:from])}"
1432
+ end
1433
+
1434
+ # The SQL fragment specifying the columns and values to SET.
1435
+ def update_set_sql(sql)
1436
+ values = opts[:values]
1437
+ set = if values.is_a?(Hash)
1438
+ values = opts[:defaults].merge(values) if opts[:defaults]
1439
+ values = values.merge(opts[:overrides]) if opts[:overrides]
1440
+ # get values from hash
1441
+ values.map do |k, v|
1442
+ "#{[String, Symbol].any?{|c| k.is_a?(c)} ? quote_identifier(k) : literal(k)} = #{literal(v)}"
1443
+ end.join(COMMA_SEPARATOR)
1444
+ else
1445
+ # copy values verbatim
1446
+ values
1447
+ end
1448
+ sql << " SET #{set}"
1383
1449
  end
1384
1450
  end
1385
1451
  end