sequel 3.1.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/CHANGELOG +76 -0
  2. data/Rakefile +2 -2
  3. data/bin/sequel +9 -4
  4. data/doc/opening_databases.rdoc +279 -0
  5. data/doc/release_notes/3.2.0.txt +268 -0
  6. data/doc/virtual_rows.rdoc +42 -51
  7. data/lib/sequel/adapters/ado.rb +2 -5
  8. data/lib/sequel/adapters/db2.rb +5 -0
  9. data/lib/sequel/adapters/do.rb +3 -0
  10. data/lib/sequel/adapters/firebird.rb +6 -4
  11. data/lib/sequel/adapters/informix.rb +5 -3
  12. data/lib/sequel/adapters/jdbc.rb +10 -8
  13. data/lib/sequel/adapters/jdbc/h2.rb +17 -4
  14. data/lib/sequel/adapters/mysql.rb +6 -19
  15. data/lib/sequel/adapters/odbc.rb +14 -18
  16. data/lib/sequel/adapters/openbase.rb +8 -0
  17. data/lib/sequel/adapters/shared/mssql.rb +14 -8
  18. data/lib/sequel/adapters/shared/mysql.rb +53 -28
  19. data/lib/sequel/adapters/shared/oracle.rb +21 -12
  20. data/lib/sequel/adapters/shared/postgres.rb +46 -26
  21. data/lib/sequel/adapters/shared/progress.rb +10 -5
  22. data/lib/sequel/adapters/shared/sqlite.rb +28 -12
  23. data/lib/sequel/adapters/sqlite.rb +4 -3
  24. data/lib/sequel/adapters/utils/stored_procedures.rb +18 -9
  25. data/lib/sequel/connection_pool.rb +4 -3
  26. data/lib/sequel/database.rb +110 -10
  27. data/lib/sequel/database/schema_sql.rb +12 -3
  28. data/lib/sequel/dataset.rb +40 -3
  29. data/lib/sequel/dataset/convenience.rb +0 -11
  30. data/lib/sequel/dataset/graph.rb +25 -11
  31. data/lib/sequel/dataset/sql.rb +176 -68
  32. data/lib/sequel/extensions/migration.rb +37 -21
  33. data/lib/sequel/extensions/schema_dumper.rb +8 -61
  34. data/lib/sequel/model.rb +3 -3
  35. data/lib/sequel/model/associations.rb +9 -1
  36. data/lib/sequel/model/base.rb +8 -1
  37. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  38. data/lib/sequel/sql.rb +125 -18
  39. data/lib/sequel/version.rb +1 -1
  40. data/spec/adapters/ado_spec.rb +1 -0
  41. data/spec/adapters/firebird_spec.rb +1 -0
  42. data/spec/adapters/informix_spec.rb +1 -0
  43. data/spec/adapters/mysql_spec.rb +23 -8
  44. data/spec/adapters/oracle_spec.rb +1 -0
  45. data/spec/adapters/postgres_spec.rb +52 -4
  46. data/spec/adapters/spec_helper.rb +2 -2
  47. data/spec/adapters/sqlite_spec.rb +2 -1
  48. data/spec/core/connection_pool_spec.rb +16 -0
  49. data/spec/core/database_spec.rb +174 -0
  50. data/spec/core/dataset_spec.rb +121 -26
  51. data/spec/core/expression_filters_spec.rb +156 -0
  52. data/spec/core/object_graph_spec.rb +20 -1
  53. data/spec/core/schema_spec.rb +5 -5
  54. data/spec/extensions/migration_spec.rb +140 -74
  55. data/spec/extensions/schema_dumper_spec.rb +3 -69
  56. data/spec/extensions/single_table_inheritance_spec.rb +6 -0
  57. data/spec/integration/dataset_test.rb +84 -2
  58. data/spec/integration/schema_test.rb +24 -5
  59. data/spec/integration/spec_helper.rb +8 -6
  60. data/spec/model/eager_loading_spec.rb +9 -0
  61. data/spec/model/record_spec.rb +35 -8
  62. metadata +8 -7
  63. data/lib/sequel/adapters/utils/date_format.rb +0 -21
  64. data/lib/sequel/adapters/utils/savepoint_transactions.rb +0 -80
  65. data/lib/sequel/adapters/utils/unsupported.rb +0 -50
@@ -7,6 +7,7 @@ module Sequel
7
7
  COLUMN_REF_RE2 = /\A([\w ]+)___([\w ]+)\z/.freeze
8
8
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
9
9
  COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
10
+ DATASET_ALIAS_BASE_NAME = 't'.freeze
10
11
  INSERT_SQL_BASE="INSERT INTO ".freeze
11
12
  IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
12
13
  IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
@@ -15,9 +16,10 @@ module Sequel
15
16
  QUALIFY_KEYS = [:select, :where, :having, :order, :group]
16
17
  QUESTION_MARK = '?'.freeze
17
18
  STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
18
- SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having compounds order limit'.freeze
19
+ SELECT_CLAUSE_ORDER = %w'with distinct columns from join where group having compounds order limit'.freeze
19
20
  TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
20
21
  WILDCARD = '*'.freeze
22
+ SQL_WITH = "WITH ".freeze
21
23
 
22
24
  # Adds an further filter to an existing filter using AND. If no filter
23
25
  # exists an error is raised. This method is identical to #filter except
@@ -63,8 +65,15 @@ module Sequel
63
65
  def complex_expression_sql(op, args)
64
66
  case op
65
67
  when *IS_OPERATORS
66
- v = IS_LITERALS[args.at(1)] || raise(Error, 'Invalid argument used for IS operator')
67
- "(#{literal(args.at(0))} #{op} #{v})"
68
+ r = args.at(1)
69
+ if r.nil? || supports_is_true?
70
+ raise(InvalidOperation, 'Invalid argument used for IS operator') unless v = IS_LITERALS[r]
71
+ "(#{literal(args.at(0))} #{op} #{v})"
72
+ elsif op == :IS
73
+ complex_expression_sql(:"=", args)
74
+ else
75
+ complex_expression_sql(:OR, [SQL::BooleanExpression.new(:"!=", *args), SQL::BooleanExpression.new(:IS, args.at(0), nil)])
76
+ end
68
77
  when *TWO_ARITY_OPERATORS
69
78
  "(#{literal(args.at(0))} #{op} #{literal(args.at(1))})"
70
79
  when *N_ARITY_OPERATORS
@@ -76,7 +85,7 @@ module Sequel
76
85
  when :'B~'
77
86
  "~#{literal(args.at(0))}"
78
87
  else
79
- raise(Sequel::Error, "invalid operator #{op}")
88
+ raise(InvalidOperation, "invalid operator #{op}")
80
89
  end
81
90
  end
82
91
 
@@ -113,21 +122,31 @@ module Sequel
113
122
  # The DISTINCT clause is used to remove duplicate rows from the
114
123
  # output. If arguments are provided, uses a DISTINCT ON clause,
115
124
  # in which case it will only be distinct on those columns, instead
116
- # of all returned columns.
125
+ # of all returned columns. Raises an error if arguments
126
+ # are given and DISTINCT ON is not supported.
117
127
  #
118
128
  # dataset.distinct # SQL: SELECT DISTINCT * FROM items
119
129
  # dataset.order(:id).distinct(:id) # SQL: SELECT DISTINCT ON (id) * FROM items ORDER BY id
120
130
  def distinct(*args)
131
+ raise(InvalidOperation, "DISTINCT ON not supported") if !args.empty? && !supports_distinct_on?
121
132
  clone(:distinct => args)
122
133
  end
123
134
 
124
- # Adds an EXCEPT clause using a second dataset object. If all is true the
125
- # clause used is EXCEPT ALL, which may return duplicate rows.
135
+ # Adds an EXCEPT clause using a second dataset object.
136
+ # An EXCEPT compound dataset returns all rows in the current dataset
137
+ # that are not in the given dataset.
138
+ # Raises an InvalidOperation if the operation is not supported.
139
+ # Options:
140
+ # * :all - Set to true to use EXCEPT ALL instead of EXCEPT, so duplicate rows can occur
141
+ # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
126
142
  #
127
143
  # DB[:items].except(DB[:other_items]).sql
128
144
  # #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
129
- def except(dataset, all = false)
130
- compound_clone(:except, dataset, all)
145
+ def except(dataset, opts={})
146
+ opts = {:all=>opts} unless opts.is_a?(Hash)
147
+ raise(InvalidOperation, "EXCEPT not supported") unless supports_intersect_except?
148
+ raise(InvalidOperation, "EXCEPT ALL not supported") if opts[:all] && !supports_intersect_except_all?
149
+ compound_clone(:except, dataset, opts)
131
150
  end
132
151
 
133
152
  # Performs the inverse of Dataset#filter.
@@ -204,14 +223,14 @@ module Sequel
204
223
 
205
224
  # The first source (primary table) for this dataset. If the dataset doesn't
206
225
  # have a table, raises an error. If the table is aliased, returns the aliased name.
207
- def first_source
226
+ def first_source_alias
208
227
  source = @opts[:from]
209
228
  if source.nil? || source.empty?
210
229
  raise Error, 'No source specified for query'
211
230
  end
212
231
  case s = source.first
213
- when Hash
214
- s.values.first
232
+ when SQL::AliasedExpression
233
+ s.aliaz
215
234
  when Symbol
216
235
  sch, table, aliaz = split_symbol(s)
217
236
  aliaz ? aliaz.to_sym : s
@@ -219,6 +238,7 @@ module Sequel
219
238
  s
220
239
  end
221
240
  end
241
+ alias first_source first_source_alias
222
242
 
223
243
  # Returns a copy of the dataset with the source changed.
224
244
  #
@@ -226,9 +246,31 @@ module Sequel
226
246
  # dataset.from(:blah) # SQL: SELECT * FROM blah
227
247
  # dataset.from(:blah, :foo) # SQL: SELECT * FROM blah, foo
228
248
  def from(*source)
229
- clone(:from=>source.empty? ? nil : source)
249
+ table_alias_num = 0
250
+ sources = []
251
+ source.each do |s|
252
+ case s
253
+ when Hash
254
+ s.each{|k,v| sources << SQL::AliasedExpression.new(k,v)}
255
+ when Dataset
256
+ sources << SQL::AliasedExpression.new(s, dataset_alias(table_alias_num+=1))
257
+ when Symbol
258
+ sch, table, aliaz = split_symbol(s)
259
+ if aliaz
260
+ s = sch ? SQL::QualifiedIdentifier.new(sch.to_sym, table.to_sym) : SQL::Identifier.new(table.to_sym)
261
+ sources << SQL::AliasedExpression.new(s, aliaz.to_sym)
262
+ else
263
+ sources << s
264
+ end
265
+ else
266
+ sources << s
267
+ end
268
+ end
269
+ o = {:from=>sources.empty? ? nil : sources}
270
+ o[:num_dataset_sources] = table_alias_num if table_alias_num > 0
271
+ clone(o)
230
272
  end
231
-
273
+
232
274
  # Returns a dataset selecting from the current dataset.
233
275
  #
234
276
  # ds = DB[:items].order(:name)
@@ -237,8 +279,7 @@ module Sequel
237
279
  def from_self
238
280
  fs = {}
239
281
  @opts.keys.each{|k| fs[k] = nil}
240
- fs[:from] = [self]
241
- clone(fs)
282
+ clone(fs).from(self)
242
283
  end
243
284
 
244
285
  # SQL fragment specifying an SQL function call
@@ -319,7 +360,7 @@ module Sequel
319
360
  if values.empty?
320
361
  insert_default_values_sql
321
362
  else
322
- "#{insert_sql_base}#{from} VALUES #{literal(values)}"
363
+ "#{insert_sql_base}#{from} VALUES #{literal(values)}#{insert_sql_suffix}"
323
364
  end
324
365
  when Hash
325
366
  values = @opts[:defaults].merge(values) if @opts[:defaults]
@@ -332,20 +373,28 @@ module Sequel
332
373
  fl << literal(String === k ? k.to_sym : k)
333
374
  vl << literal(v)
334
375
  end
335
- "#{insert_sql_base}#{from} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
376
+ "#{insert_sql_base}#{from} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})#{insert_sql_suffix}"
336
377
  end
337
378
  when Dataset
338
- "#{insert_sql_base}#{from} #{literal(values)}"
379
+ "#{insert_sql_base}#{from} #{literal(values)}#{insert_sql_suffix}"
339
380
  end
340
381
  end
341
382
 
342
- # Adds an INTERSECT clause using a second dataset object. If all is true
343
- # the clause used is INTERSECT ALL, which may return duplicate rows.
383
+ # Adds an INTERSECT clause using a second dataset object.
384
+ # An INTERSECT compound dataset returns all rows in both the current dataset
385
+ # and the given dataset.
386
+ # Raises an InvalidOperation if the operation is not supported.
387
+ # Options:
388
+ # * :all - Set to true to use INTERSECT ALL instead of INTERSECT, so duplicate rows can occur
389
+ # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
344
390
  #
345
391
  # DB[:items].intersect(DB[:other_items]).sql
346
392
  # #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
347
- def intersect(dataset, all = false)
348
- compound_clone(:intersect, dataset, all)
393
+ def intersect(dataset, opts={})
394
+ opts = {:all=>opts} unless opts.is_a?(Hash)
395
+ raise(InvalidOperation, "INTERSECT not supported") unless supports_intersect_except?
396
+ raise(InvalidOperation, "INTERSECT ALL not supported") if opts[:all] && !supports_intersect_except_all?
397
+ compound_clone(:intersect, dataset, opts)
349
398
  end
350
399
 
351
400
  # Inverts the current filter
@@ -421,7 +470,7 @@ module Sequel
421
470
  if Dataset === table
422
471
  if table_alias.nil?
423
472
  table_alias_num = (@opts[:num_dataset_sources] || 0) + 1
424
- table_alias = "t#{table_alias_num}"
473
+ table_alias = dataset_alias(table_alias_num)
425
474
  end
426
475
  table_name = table_alias
427
476
  else
@@ -435,7 +484,7 @@ module Sequel
435
484
  raise(Sequel::Error, "can't use a block if providing an array of symbols as expr") if block_given?
436
485
  SQL::JoinUsingClause.new(expr, type, table, table_alias)
437
486
  else
438
- last_alias ||= @opts[:last_joined_table] || (first_source.is_a?(Dataset) ? 't1' : first_source)
487
+ last_alias ||= @opts[:last_joined_table] || first_source_alias
439
488
  if Sequel.condition_specifier?(expr)
440
489
  expr = expr.collect do |k, v|
441
490
  k = qualified_column_name(k, table_name) if k.is_a?(Symbol)
@@ -597,6 +646,11 @@ module Sequel
597
646
  [qcr.table, qcr.column].map{|x| [SQL::QualifiedIdentifier, SQL::Identifier, Symbol].any?{|c| x.is_a?(c)} ? literal(x) : quote_identifier(x)}.join('.')
598
647
  end
599
648
 
649
+ # Qualify to the given table, or first source if not table is given.
650
+ def qualify(table=first_source)
651
+ qualify_to(table)
652
+ end
653
+
600
654
  # Return a copy of the dataset with unqualified identifiers in the
601
655
  # SELECT, WHERE, GROUP, HAVING, and ORDER clauses qualified by the
602
656
  # given table. If no columns are currently selected, select all
@@ -730,13 +784,18 @@ module Sequel
730
784
  clone(:where => nil, :having => nil)
731
785
  end
732
786
 
733
- # Adds a UNION clause using a second dataset object. If all is true the
734
- # clause used is UNION ALL, which may return duplicate rows.
787
+ # Adds a UNION clause using a second dataset object.
788
+ # A UNION compound dataset returns all rows in either the current dataset
789
+ # or the given dataset.
790
+ # Options:
791
+ # * :all - Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
792
+ # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
735
793
  #
736
794
  # DB[:items].union(DB[:other_items]).sql
737
795
  # #=> "SELECT * FROM items UNION SELECT * FROM other_items"
738
- def union(dataset, all = false)
739
- compound_clone(:union, dataset, all)
796
+ def union(dataset, opts={})
797
+ opts = {:all=>opts} unless opts.is_a?(Hash)
798
+ compound_clone(:union, dataset, opts)
740
799
  end
741
800
 
742
801
  # Returns a copy of the dataset with no order.
@@ -783,7 +842,7 @@ module Sequel
783
842
 
784
843
  sql
785
844
  end
786
-
845
+
787
846
  # Add a condition to the WHERE clause. See #filter for argument types.
788
847
  #
789
848
  # dataset.group(:a).having(:a).filter(:b) # SELECT * FROM items GROUP BY a HAVING a AND b
@@ -792,6 +851,50 @@ module Sequel
792
851
  _filter(:where, *cond, &block)
793
852
  end
794
853
 
854
+ # The SQL fragment for the given window's options.
855
+ def window_sql(opts)
856
+ raise(Error, 'This dataset does not support window functions') unless supports_window_functions?
857
+ window = literal(opts[:window]) if opts[:window]
858
+ partition = "PARTITION BY #{expression_list(Array(opts[:partition]))}" if opts[:partition]
859
+ order = "ORDER BY #{expression_list(Array(opts[:order]))}" if opts[:order]
860
+ frame = case opts[:frame]
861
+ when nil
862
+ nil
863
+ when :all
864
+ "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"
865
+ when :rows
866
+ "ROWS UNBOUNDED PRECEDING"
867
+ else
868
+ raise Error, "invalid window frame clause, should be :all, :rows, or nil"
869
+ end
870
+ "(#{[window, partition, order, frame].compact.join(' ')})"
871
+ end
872
+
873
+ # The SQL fragment for the given window function's function and window.
874
+ def window_function_sql(function, window)
875
+ "#{literal(function)} OVER #{literal(window)}"
876
+ end
877
+
878
+ # Add a simple common table expression (CTE) with the given name and a dataset that defines the CTE.
879
+ # A common table expression acts as an inline view for the query.
880
+ # Options:
881
+ # * :args - Specify the arguments/columns for the CTE, should be an array of symbols.
882
+ # * :recursive - Specify that this is a recursive CTE
883
+ def with(name, dataset, opts={})
884
+ raise(Error, 'This datatset does not support common table expressions') unless supports_cte?
885
+ clone(:with=>(@opts[:with]||[]) + [opts.merge(:name=>name, :dataset=>dataset)])
886
+ end
887
+
888
+ # Add a recursive common table expression (CTE) with the given name, a dataset that
889
+ # defines the nonrecursive part of the CTE, and a dataset that defines the recursive part
890
+ # of the CTE. Options:
891
+ # * :args - Specify the arguments/columns for the CTE, should be an array of symbols.
892
+ # * :union_all - Set to false to use UNION instead of UNION ALL combining the nonrecursive and recursive parts.
893
+ def with_recursive(name, nonrecursive, recursive, opts={})
894
+ raise(Error, 'This datatset does not support common table expressions') unless supports_cte?
895
+ clone(:with=>(@opts[:with]||[]) + [opts.merge(:recursive=>true, :name=>name, :dataset=>nonrecursive.union(recursive, {:all=>opts[:union_all] != false, :from_self=>false}))])
896
+ end
897
+
795
898
  # Returns a copy of the dataset with the static SQL used. This is useful if you want
796
899
  # to keep the same row_proc/graph, but change the SQL used to custom SQL.
797
900
  #
@@ -814,13 +917,6 @@ module Sequel
814
917
  (@opts[:limit] || @opts[:order]) ? from_self : self
815
918
  end
816
919
 
817
- # Returns a table reference for use in the FROM clause. Returns an SQL subquery
818
- # frgament with an optional table alias.
819
- def to_table_reference(table_alias=nil)
820
- s = "(#{sql})"
821
- table_alias ? as_sql(s, table_alias) : s
822
- end
823
-
824
920
  private
825
921
 
826
922
  # Internal filter method so it works on either the having or where clauses.
@@ -830,6 +926,11 @@ module Sequel
830
926
  cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
831
927
  clone(clause => cond)
832
928
  end
929
+
930
+ # Do a simple join of the arguments (which should be strings or symbols) separated by commas
931
+ def argument_list(args)
932
+ args.join(COMMA_SEPARATOR)
933
+ end
833
934
 
834
935
  # SQL fragment for specifying an alias. expression should already be literalized.
835
936
  def as_sql(expression, aliaz)
@@ -843,10 +944,16 @@ module Sequel
843
944
  end
844
945
 
845
946
  # Add the dataset to the list of compounds
846
- def compound_clone(type, dataset, all)
847
- compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, all]]).from_self
947
+ def compound_clone(type, dataset, opts)
948
+ ds = compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, opts[:all]]])
949
+ opts[:from_self] == false ? ds : ds.from_self
848
950
  end
849
951
 
952
+ # The alias to use for datasets, takes a number to make sure the name is unique.
953
+ def dataset_alias(number)
954
+ :"#{DATASET_ALIAS_BASE_NAME}#{number}"
955
+ end
956
+
850
957
  # Converts an array of expressions into a comma separated string of
851
958
  # expressions.
852
959
  def expression_list(columns)
@@ -902,6 +1009,11 @@ module Sequel
902
1009
  "#{insert_sql_base}#{source_list(@opts[:from])} DEFAULT VALUES"
903
1010
  end
904
1011
 
1012
+ # SQL statement for end of an INSERT statement
1013
+ def insert_sql_suffix
1014
+ nil
1015
+ end
1016
+
905
1017
  # Inverts the given order by breaking it into a list of column references
906
1018
  # and inverting them.
907
1019
  #
@@ -950,12 +1062,12 @@ module Sequel
950
1062
 
951
1063
  # SQL fragment for Date, using the ISO8601 format.
952
1064
  def literal_date(v)
953
- "'#{v}'"
1065
+ requires_sql_standard_datetimes? ? v.strftime("DATE '%Y-%m-%d'") : "'#{v}'"
954
1066
  end
955
1067
 
956
1068
  # SQL fragment for DateTime, using the ISO8601 format.
957
1069
  def literal_datetime(v)
958
- "'#{v}'"
1070
+ requires_sql_standard_datetimes? ? v.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'") : "'#{v}'"
959
1071
  end
960
1072
 
961
1073
  # SQL fragment for SQL::Expression, result depends on the specific type of expression.
@@ -1009,7 +1121,7 @@ module Sequel
1009
1121
 
1010
1122
  # SQL fragment for Time, uses the ISO8601 format.
1011
1123
  def literal_time(v)
1012
- "'#{v.iso8601}'"
1124
+ requires_sql_standard_datetimes? ? v.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'") : "'#{v.iso8601}'"
1013
1125
  end
1014
1126
 
1015
1127
  # SQL fragment for true.
@@ -1074,6 +1186,15 @@ module Sequel
1074
1186
  SQL::SQLArray.new(qualified_expression(e.array, table))
1075
1187
  when SQL::Subscript
1076
1188
  SQL::Subscript.new(qualified_expression(e.f, table), qualified_expression(e.sub, table))
1189
+ when SQL::WindowFunction
1190
+ SQL::WindowFunction.new(qualified_expression(e.function, table), qualified_expression(e.window, table))
1191
+ when SQL::Window
1192
+ o = e.opts.dup
1193
+ o[:partition] = qualified_expression(o[:partition], table) if o[:partition]
1194
+ o[:order] = qualified_expression(o[:order], table) if o[:order]
1195
+ SQL::Window.new(o)
1196
+ when SQL::PlaceholderLiteralString
1197
+ SQL::PlaceholderLiteralString.new(e.str, qualified_expression(e.args, table), e.parens)
1077
1198
  else
1078
1199
  e
1079
1200
  end
@@ -1142,23 +1263,21 @@ module Sequel
1142
1263
  def select_where_sql(sql)
1143
1264
  sql << " WHERE #{literal(@opts[:where])}" if @opts[:where]
1144
1265
  end
1266
+
1267
+ def select_with_sql(sql)
1268
+ ws = opts[:with]
1269
+ return if !ws || ws.empty?
1270
+ 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}")
1271
+ end
1272
+
1273
+ def select_with_sql_base
1274
+ SQL_WITH
1275
+ end
1145
1276
 
1146
1277
  # Converts an array of source names into into a comma separated list.
1147
1278
  def source_list(source)
1148
- if source.nil? || source.empty?
1149
- raise Error, 'No source specified for query'
1150
- end
1151
- auto_alias_count = @opts[:num_dataset_sources] || 0
1152
- m = source.map do |s|
1153
- case s
1154
- when Dataset
1155
- auto_alias_count += 1
1156
- s.to_table_reference("t#{auto_alias_count}")
1157
- else
1158
- table_ref(s)
1159
- end
1160
- end
1161
- m.join(COMMA_SEPARATOR)
1279
+ raise(Error, 'No source specified for query') if source.nil? || source.empty?
1280
+ source.map{|s| table_ref(s)}.join(COMMA_SEPARATOR)
1162
1281
  end
1163
1282
 
1164
1283
  # Splits the symbol into three parts. Each part will
@@ -1193,18 +1312,7 @@ module Sequel
1193
1312
 
1194
1313
  # SQL fragment specifying a table name.
1195
1314
  def table_ref(t)
1196
- case t
1197
- when Symbol
1198
- literal_symbol(t)
1199
- when Dataset
1200
- t.to_table_reference
1201
- when Hash
1202
- t.map{|k, v| as_sql(table_ref(k), v)}.join(COMMA_SEPARATOR)
1203
- when String
1204
- quote_identifier(t)
1205
- else
1206
- literal(t)
1207
- end
1315
+ t.is_a?(String) ? quote_identifier(t) : literal(t)
1208
1316
  end
1209
1317
  end
1210
1318
  end
@@ -131,27 +131,40 @@ module Sequel
131
131
  #
132
132
  # Sequel::Migrator.apply(DB, '.', 5, 1)
133
133
  module Migrator
134
+ DEFAULT_SCHEMA_COLUMN = :version
135
+ DEFAULT_SCHEMA_TABLE = :schema_info
134
136
  MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z/.freeze
135
137
  MIGRATION_SPLITTER = '_'.freeze
136
138
 
137
- # Migrates the supplied database in the specified directory from the
138
- # current version to the target version. If no current version is
139
- # supplied, it is extracted from a schema_info table. The schema_info
140
- # table is automatically created and maintained by the apply function.
139
+ # Wrapper for run, maintaining backwards API compatibility
141
140
  def self.apply(db, directory, target = nil, current = nil)
142
- # determine current and target version and direction
143
- current ||= get_current_migration_version(db)
144
- target ||= latest_migration_version(directory)
145
- raise Error, "No current version available" if current.nil?
146
- raise Error, "No target version available" if target.nil?
141
+ run(db, directory, :target => target, :current => current)
142
+ end
143
+
144
+ # Migrates the supplied database using the migration files in the the specified directory. Options:
145
+ # * :column - The column in the :table argument storing the migration version (default: :version).
146
+ # * :current - The current version of the database. If not given, it is retrieved from the database
147
+ # using the :table and :column options.
148
+ # * :table - The table containing the schema version (default: :schema_info).
149
+ # * :target - The target version to which to migrate. If not given, migrates to the maximum version.
150
+ #
151
+ # Examples:
152
+ # Sequel::Migrator.run(DB, "migrations")
153
+ # Sequel::Migrator.run(DB, "migrations", :target=>15, :current=>10)
154
+ # Sequel::Migrator.run(DB, "app1/migrations", :column=> :app2_version)
155
+ # Sequel::Migrator.run(DB, "app2/migrations", :column => :app2_version, :table=>:schema_info2)
156
+ def self.run(db, directory, opts={})
157
+ raise(Error, "Must supply a valid migration path") unless directory and File.directory?(directory)
158
+ raise(Error, "No current version available") unless current = opts[:current] || get_current_migration_version(db, opts)
159
+ raise(Error, "No target version available") unless target = opts[:target] || latest_migration_version(directory)
147
160
 
148
161
  direction = current < target ? :up : :down
149
162
 
150
163
  classes = migration_classes(directory, target, current, direction)
151
-
164
+
152
165
  db.transaction do
153
166
  classes.each {|c| c.apply(db, direction)}
154
- set_current_migration_version(db, target)
167
+ set_current_migration_version(db, target, opts)
155
168
  end
156
169
 
157
170
  target
@@ -159,9 +172,8 @@ module Sequel
159
172
 
160
173
  # Gets the current migration version stored in the database. If no version
161
174
  # number is stored, 0 is returned.
162
- def self.get_current_migration_version(db)
163
- r = schema_info_dataset(db).first
164
- r ? r[:version] : 0
175
+ def self.get_current_migration_version(db, opts={})
176
+ (schema_info_dataset(db, opts).first || {})[opts[:column] || DEFAULT_SCHEMA_COLUMN] || 0
165
177
  end
166
178
 
167
179
  # Returns the latest version available in the specified directory.
@@ -203,19 +215,23 @@ module Sequel
203
215
 
204
216
  # Returns the dataset for the schema_info table. If no such table
205
217
  # exists, it is automatically created.
206
- def self.schema_info_dataset(db)
207
- db.create_table(:schema_info) {integer :version} unless db.table_exists?(:schema_info)
208
- db[:schema_info]
218
+ def self.schema_info_dataset(db, opts={})
219
+ column = opts[:column] || DEFAULT_SCHEMA_COLUMN
220
+ table = opts[:table] || DEFAULT_SCHEMA_TABLE
221
+ db.create_table?(table){Integer column}
222
+ db.alter_table(table){add_column column, Integer} unless db.from(table).columns.include?(column)
223
+ db.from(table)
209
224
  end
210
225
 
211
226
  # Sets the current migration version stored in the database.
212
- def self.set_current_migration_version(db, version)
213
- dataset = schema_info_dataset(db)
214
- dataset.send(dataset.first ? :update : :<<, :version => version)
227
+ def self.set_current_migration_version(db, version, opts={})
228
+ column = opts[:column] || DEFAULT_SCHEMA_COLUMN
229
+ dataset = schema_info_dataset(db, opts)
230
+ dataset.send(dataset.first ? :update : :insert, column => version)
215
231
  end
216
232
 
217
233
  # Return the integer migration version based on the filename.
218
- def self.migration_version_from_file(filename)
234
+ def self.migration_version_from_file(filename) # :nodoc:
219
235
  filename.split(MIGRATION_SPLITTER, 2).first.to_i
220
236
  end
221
237
  private_class_method :migration_version_from_file