sequel 3.4.0 → 3.5.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 (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