sequel 5.43.0 → 5.47.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +40 -0
  3. data/README.rdoc +1 -2
  4. data/doc/association_basics.rdoc +70 -11
  5. data/doc/migration.rdoc +11 -5
  6. data/doc/release_notes/5.44.0.txt +32 -0
  7. data/doc/release_notes/5.45.0.txt +34 -0
  8. data/doc/release_notes/5.46.0.txt +87 -0
  9. data/doc/release_notes/5.47.0.txt +59 -0
  10. data/doc/sql.rdoc +12 -0
  11. data/doc/testing.rdoc +5 -0
  12. data/doc/virtual_rows.rdoc +1 -1
  13. data/lib/sequel/adapters/odbc.rb +5 -1
  14. data/lib/sequel/adapters/shared/mysql.rb +17 -0
  15. data/lib/sequel/adapters/shared/postgres.rb +0 -12
  16. data/lib/sequel/adapters/shared/sqlite.rb +55 -9
  17. data/lib/sequel/core.rb +11 -0
  18. data/lib/sequel/database/schema_generator.rb +25 -46
  19. data/lib/sequel/database/schema_methods.rb +1 -1
  20. data/lib/sequel/dataset/query.rb +2 -4
  21. data/lib/sequel/dataset/sql.rb +7 -0
  22. data/lib/sequel/extensions/date_arithmetic.rb +29 -15
  23. data/lib/sequel/extensions/pg_enum.rb +1 -1
  24. data/lib/sequel/extensions/pg_loose_count.rb +3 -1
  25. data/lib/sequel/extensions/schema_dumper.rb +11 -0
  26. data/lib/sequel/model/associations.rb +275 -89
  27. data/lib/sequel/plugins/async_thread_pool.rb +1 -1
  28. data/lib/sequel/plugins/auto_validations_constraint_validations_presence_message.rb +68 -0
  29. data/lib/sequel/plugins/column_encryption.rb +20 -3
  30. data/lib/sequel/plugins/concurrent_eager_loading.rb +174 -0
  31. data/lib/sequel/plugins/many_through_many.rb +108 -9
  32. data/lib/sequel/plugins/pg_array_associations.rb +52 -38
  33. data/lib/sequel/plugins/prepared_statements.rb +10 -1
  34. data/lib/sequel/plugins/rcte_tree.rb +27 -19
  35. data/lib/sequel/plugins/timestamps.rb +1 -1
  36. data/lib/sequel/plugins/unused_associations.rb +520 -0
  37. data/lib/sequel/version.rb +1 -1
  38. metadata +14 -3
data/doc/sql.rdoc CHANGED
@@ -428,6 +428,18 @@ As you can see, these literalize with ANDs by default. You can use the <tt>Sequ
428
428
 
429
429
  Sequel.or(column1: 1, column2: 2) # (("column1" = 1) OR ("column2" = 2))
430
430
 
431
+ As you can see in the above examples, <tt>Sequel.|</tt> and <tt>Sequel.or</tt> work differently.
432
+ <tt>Sequel.|</tt> is for combining an arbitrary number of expressions using OR. If you pass a single
433
+ argument, <tt>Sequel.|</tt> will just convert it to a Sequel expression, similar to <tt>Sequel.expr</tt>.
434
+ <tt>Sequel.or</tt> is for taking a single hash or array of two element arrays and combining the
435
+ elements of that single argument using OR instead of AND:
436
+
437
+ Sequel.|(column1: 1, column2: 2) # (("column1" = 1) AND ("column2" = 2))
438
+ Sequel.or(column1: 1, column2: 2) # (("column1" = 1) OR ("column2" = 2))
439
+
440
+ Sequel.|({column1: 1}, {column2: 2}) # (("column1" = 1) OR ("column2" = 2))
441
+ Sequel.or({column1: 1}, {column2: 2}) # ArgumentError
442
+
431
443
  You've already seen the <tt>Sequel.negate</tt> method, which will use ANDs if multiple entries are used:
432
444
 
433
445
  Sequel.negate(column1: 1, column2: 2) # (("column1" != 1) AND ("column2" != 2))
data/doc/testing.rdoc CHANGED
@@ -162,6 +162,7 @@ SEQUEL_ASYNC_THREAD_POOL_PREEMPT :: Use the async_thread_pool extension when run
162
162
  SEQUEL_COLUMNS_INTROSPECTION :: Use the columns_introspection extension when running the specs
163
163
  SEQUEL_CONNECTION_VALIDATOR :: Use the connection validator extension when running the specs
164
164
  SEQUEL_DUPLICATE_COLUMNS_HANDLER :: Use the duplicate columns handler extension with value given when running the specs
165
+ SEQUEL_CONCURRENT_EAGER_LOADING :: Use the async_thread_pool extension and concurrent_eager_loading plugin when running the specs
165
166
  SEQUEL_ERROR_SQL :: Use the error_sql extension when running the specs
166
167
  SEQUEL_INDEX_CACHING :: Use the index_caching extension when running the specs
167
168
  SEQUEL_FIBER_CONCURRENCY :: Use the fiber_concurrency extension when running the adapter and integration specs
@@ -174,6 +175,10 @@ SEQUEL_NO_CACHE_ASSOCIATIONS :: Don't cache association metadata when running th
174
175
  SEQUEL_CHECK_PENDING :: Try running all specs (note, can cause lockups for some adapters), and raise errors for skipped specs that don't fail
175
176
  SEQUEL_NO_PENDING :: Don't skip any specs, try running all specs (note, can cause lockups for some adapters)
176
177
  SEQUEL_PG_TIMESTAMPTZ :: Use the pg_timestamptz extension when running the postgres specs
178
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_0_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
179
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_1_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
180
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_2_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
181
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_3_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
177
182
  SEQUEL_SPLIT_SYMBOLS :: Turn on symbol splitting when running the adapter and integration specs
178
183
  SEQUEL_SYNCHRONIZE_SQL :: Use the synchronize_sql extension when running the specs
179
184
  SEQUEL_TZINFO_VERSION :: Force the given tzinfo version when running the specs (e.g. '>=2')
@@ -54,7 +54,7 @@ methods in the surrounding scope. For example:
54
54
 
55
55
  # Regular block
56
56
  ds.where{|o| o.c > a - b + @d}
57
- # WHERE (c > 100)
57
+ # WHERE (c > 110)
58
58
 
59
59
  # Instance-evaled block
60
60
  ds.where{c > a - b + @d}
@@ -94,7 +94,11 @@ module Sequel
94
94
  self.columns = columns
95
95
  s.each do |row|
96
96
  hash = {}
97
- cols.each{|n,t,j| hash[n] = convert_odbc_value(row[j], t)}
97
+ cols.each do |n,t,j|
98
+ v = row[j]
99
+ # We can assume v is not false, so this shouldn't convert false to nil.
100
+ hash[n] = (convert_odbc_value(v, t) if v)
101
+ end
98
102
  yield hash
99
103
  end
100
104
  end
@@ -187,6 +187,15 @@ module Sequel
187
187
  def views(opts=OPTS)
188
188
  full_tables('VIEW', opts)
189
189
  end
190
+
191
+ # Renames multiple tables in a single call.
192
+ #
193
+ # DB.rename_tables [:items, :old_items], [:other_items, :old_other_items]
194
+ # # RENAME TABLE items TO old_items, other_items TO old_other_items
195
+ def rename_tables(*renames)
196
+ execute_ddl(rename_tables_sql(renames))
197
+ renames.each{|from,| remove_cached_schema(from)}
198
+ end
190
199
 
191
200
  private
192
201
 
@@ -473,6 +482,14 @@ module Sequel
473
482
  schema(table).select{|a| a[1][:primary_key]}.map{|a| a[0]}
474
483
  end
475
484
 
485
+ # SQL statement for renaming multiple tables.
486
+ def rename_tables_sql(renames)
487
+ rename_tos = renames.map do |from, to|
488
+ "#{quote_schema_table(from)} TO #{quote_schema_table(to)}"
489
+ end.join(', ')
490
+ "RENAME TABLE #{rename_tos}"
491
+ end
492
+
476
493
  # Rollback the currently open XA transaction
477
494
  def rollback_transaction(conn, opts=OPTS)
478
495
  if (s = opts[:prepare]) && savepoint_level(conn) <= 1
@@ -2141,18 +2141,6 @@ module Sequel
2141
2141
  opts[:with].any?{|w| w[:recursive]} ? "WITH RECURSIVE " : super
2142
2142
  end
2143
2143
 
2144
- # Support WITH AS [NOT] MATERIALIZED if :materialized option is used.
2145
- def select_with_sql_prefix(sql, w)
2146
- super
2147
-
2148
- case w[:materialized]
2149
- when true
2150
- sql << "MATERIALIZED "
2151
- when false
2152
- sql << "NOT MATERIALIZED "
2153
- end
2154
- end
2155
-
2156
2144
  # The version of the database server
2157
2145
  def server_version
2158
2146
  db.server_version(@opts[:server])
@@ -239,8 +239,12 @@ module Sequel
239
239
  super
240
240
  end
241
241
  when :drop_column
242
- ocp = lambda{|oc| oc.delete_if{|c| c.to_s == op[:name].to_s}}
243
- duplicate_table(table, :old_columns_proc=>ocp){|columns| columns.delete_if{|s| s[:name].to_s == op[:name].to_s}}
242
+ if sqlite_version >= 33500
243
+ super
244
+ else
245
+ ocp = lambda{|oc| oc.delete_if{|c| c.to_s == op[:name].to_s}}
246
+ duplicate_table(table, :old_columns_proc=>ocp){|columns| columns.delete_if{|s| s[:name].to_s == op[:name].to_s}}
247
+ end
244
248
  when :rename_column
245
249
  if sqlite_version >= 32500
246
250
  super
@@ -424,10 +428,10 @@ module Sequel
424
428
  skip_indexes = []
425
429
  indexes(table, :only_autocreated=>true).each do |name, h|
426
430
  skip_indexes << name
427
- if h[:unique]
431
+ if h[:unique] && !opts[:no_unique]
428
432
  if h[:columns].length == 1
429
433
  unique_columns.concat(h[:columns])
430
- elsif h[:columns].map(&:to_s) != pks && !opts[:no_unique]
434
+ elsif h[:columns].map(&:to_s) != pks
431
435
  constraints << {:type=>:unique, :columns=>h[:columns]}
432
436
  end
433
437
  end
@@ -558,10 +562,10 @@ module Sequel
558
562
  EXTRACT_MAP = {:year=>"'%Y'", :month=>"'%m'", :day=>"'%d'", :hour=>"'%H'", :minute=>"'%M'", :second=>"'%f'"}.freeze
559
563
  EXTRACT_MAP.each_value(&:freeze)
560
564
 
561
- Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
562
- Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 30803', %w'with insert conflict into columns values on_conflict'], ["else", %w'insert conflict into columns values']])
565
+ Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 33500', %w'with delete from where returning'], ['elsif db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
566
+ Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 33500', %w'with insert conflict into columns values on_conflict returning'], ['elsif db.sqlite_version >= 30803', %w'with insert conflict into columns values on_conflict'], ["else", %w'insert conflict into columns values']])
563
567
  Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'with values compounds'], ['else', %w'with select distinct columns from join where group having window compounds order limit lock']])
564
- Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 33300', %w'with update table set from where'], ['elsif db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
568
+ Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 33500', %w'with update table set from where returning'], ['elsif db.sqlite_version >= 33300', %w'with update table set from where'], ['elsif db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
565
569
 
566
570
  def cast_sql_append(sql, expr, type)
567
571
  if type == Time or type == DateTime
@@ -635,8 +639,8 @@ module Sequel
635
639
  # SQLite performs a TRUNCATE style DELETE if no filter is specified.
636
640
  # Since we want to always return the count of records, add a condition
637
641
  # that is always true and then delete.
638
- def delete
639
- @opts[:where] ? super : where(1=>1).delete
642
+ def delete(&block)
643
+ @opts[:where] ? super : where(1=>1).delete(&block)
640
644
  end
641
645
 
642
646
  # Return an array of strings specifying a query explanation for a SELECT of the
@@ -657,6 +661,21 @@ module Sequel
657
661
  super
658
662
  end
659
663
 
664
+ # Support insert select for associations, so that the model code can use
665
+ # returning instead of a separate query.
666
+ def insert_select(*values)
667
+ return unless supports_insert_select?
668
+ # Handle case where query does not return a row
669
+ server?(:default).with_sql_first(insert_select_sql(*values)) || false
670
+ end
671
+
672
+ # The SQL to use for an insert_select, adds a RETURNING clause to the insert
673
+ # unless the RETURNING clause is already present.
674
+ def insert_select_sql(*values)
675
+ ds = opts[:returning] ? self : returning
676
+ ds.insert_sql(*values)
677
+ end
678
+
660
679
  # SQLite uses the nonstandard ` (backtick) for quoting identifiers.
661
680
  def quoted_identifier_append(sql, c)
662
681
  sql << '`' << c.to_s.gsub('`', '``') << '`'
@@ -738,6 +757,13 @@ module Sequel
738
757
  insert_conflict(:ignore)
739
758
  end
740
759
 
760
+ # Automatically add aliases to RETURNING values to work around SQLite bug.
761
+ def returning(*values)
762
+ return super if values.empty?
763
+ raise Error, "RETURNING is not supported on #{db.database_type}" unless supports_returning?(:insert)
764
+ clone(:returning=>_returning_values(values).freeze)
765
+ end
766
+
741
767
  # SQLite 3.8.3+ supports common table expressions.
742
768
  def supports_cte?(type=:select)
743
769
  db.sqlite_version >= 30803
@@ -778,6 +804,11 @@ module Sequel
778
804
  false
779
805
  end
780
806
 
807
+ # SQLite 3.35.0 supports RETURNING on INSERT/UPDATE/DELETE.
808
+ def supports_returning?(_)
809
+ db.sqlite_version >= 33500
810
+ end
811
+
781
812
  # SQLite supports timezones in literal timestamps, since it stores them
782
813
  # as text. But using timezones in timestamps breaks SQLite datetime
783
814
  # functions, so we allow the user to override the default per database.
@@ -810,6 +841,21 @@ module Sequel
810
841
 
811
842
  private
812
843
 
844
+ # Add aliases to symbols and identifiers to work around SQLite bug.
845
+ def _returning_values(values)
846
+ values.map do |v|
847
+ case v
848
+ when Symbol
849
+ _, c, a = split_symbol(v)
850
+ a ? v : Sequel.as(v, c)
851
+ when SQL::Identifier, SQL::QualifiedIdentifier
852
+ Sequel.as(v, unqualified_column_for(v))
853
+ else
854
+ v
855
+ end
856
+ end
857
+ end
858
+
813
859
  # SQLite uses string literals instead of identifiers in AS clauses.
814
860
  def as_sql_append(sql, aliaz, column_aliases=nil)
815
861
  raise Error, "sqlite does not support derived column lists" if column_aliases
data/lib/sequel/core.rb CHANGED
@@ -176,6 +176,17 @@ module Sequel
176
176
  JSON.parse(json, :create_additions=>false)
177
177
  end
178
178
 
179
+ # If a mutex is given, synchronize access using it. If nil is given, just
180
+ # yield to the block. This is designed for cases where a mutex may or may
181
+ # not be provided.
182
+ def synchronize_with(mutex)
183
+ if mutex
184
+ mutex.synchronize{yield}
185
+ else
186
+ yield
187
+ end
188
+ end
189
+
179
190
  # Convert each item in the array to the correct type, handling multi-dimensional
180
191
  # arrays. For each element in the array or subarrays, call the converter,
181
192
  # unless the value is nil.
@@ -214,14 +214,12 @@ module Sequel
214
214
  end
215
215
 
216
216
  # Add a full text index on the given columns.
217
+ # See #index for additional options.
217
218
  #
218
219
  # PostgreSQL specific options:
219
220
  # :index_type :: Can be set to :gist to use a GIST index instead of the
220
221
  # default GIN index.
221
222
  # :language :: Set a language to use for the index (default: simple).
222
- #
223
- # Microsoft SQL Server specific options:
224
- # :key_index :: The KEY INDEX to use for the full text index.
225
223
  def full_text_index(columns, opts = OPTS)
226
224
  index(columns, opts.merge(:type => :full_text))
227
225
  end
@@ -231,35 +229,43 @@ module Sequel
231
229
  columns.any?{|c| c[:name] == name}
232
230
  end
233
231
 
234
- # Add an index on the given column(s) with the given options.
232
+ # Add an index on the given column(s) with the given options. Examples:
233
+ #
234
+ # index :name
235
+ # # CREATE INDEX table_name_index ON table (name)
236
+ #
237
+ # index [:artist_id, :name]
238
+ # # CREATE INDEX table_artist_id_name_index ON table (artist_id, name)
239
+ #
240
+ # index [:artist_id, :name], name: :foo
241
+ # # CREATE INDEX foo ON table (artist_id, name)
242
+ #
235
243
  # General options:
236
244
  #
245
+ # :include :: Include additional column values in the index, without
246
+ # actually indexing on those values (only supported by
247
+ # some databases).
237
248
  # :name :: The name to use for the index. If not given, a default name
238
249
  # based on the table and columns is used.
239
- # :type :: The type of index to use (only supported by some databases)
250
+ # :type :: The type of index to use (only supported by some databases,
251
+ # :full_text and :spatial values are handled specially).
240
252
  # :unique :: Make the index unique, so duplicate values are not allowed.
241
- # :where :: Create a partial index (only supported by some databases)
253
+ # :where :: A filter expression, used to create a partial index (only
254
+ # supported by some databases).
242
255
  #
243
256
  # PostgreSQL specific options:
244
257
  #
245
258
  # :concurrently :: Create the index concurrently, so it doesn't block
246
259
  # operations on the table while the index is being
247
260
  # built.
248
- # :opclass :: Use a specific operator class in the index.
249
- # :include :: Include additional column values in the index, without
250
- # actually indexing on those values (PostgreSQL 11+).
261
+ # :if_not_exists :: Only create the index if an index of the same name doesn't already exist.
262
+ # :opclass :: Set an opclass to use for all columns (per-column opclasses require
263
+ # custom SQL).
251
264
  # :tablespace :: Specify tablespace for index.
252
265
  #
253
266
  # Microsoft SQL Server specific options:
254
267
  #
255
- # :include :: Include additional column values in the index, without
256
- # actually indexing on those values.
257
- #
258
- # index :name
259
- # # CREATE INDEX table_name_index ON table (name)
260
- #
261
- # index [:artist_id, :name]
262
- # # CREATE INDEX table_artist_id_name_index ON table (artist_id, name)
268
+ # :key_index :: Sets the KEY INDEX to the given value.
263
269
  def index(columns, opts = OPTS)
264
270
  indexes << {:columns => Array(columns)}.merge!(opts)
265
271
  nil
@@ -325,6 +331,7 @@ module Sequel
325
331
  end
326
332
 
327
333
  # Add a spatial index on the given columns.
334
+ # See #index for additional options.
328
335
  def spatial_index(columns, opts = OPTS)
329
336
  index(columns, opts.merge(:type => :spatial))
330
337
  end
@@ -451,7 +458,7 @@ module Sequel
451
458
  end
452
459
 
453
460
  # Add a full text index on the given columns.
454
- # See CreateTableGenerator#index for available options.
461
+ # See CreateTableGenerator#full_text_index for available options.
455
462
  def add_full_text_index(columns, opts = OPTS)
456
463
  add_index(columns, {:type=>:full_text}.merge!(opts))
457
464
  end
@@ -460,34 +467,6 @@ module Sequel
460
467
  # CreateTableGenerator#index for available options.
461
468
  #
462
469
  # add_index(:artist_id) # CREATE INDEX table_artist_id_index ON table (artist_id)
463
- #
464
- # Options:
465
- #
466
- # :name :: Give a specific name for the index. Highly recommended if you plan on
467
- # dropping the index later.
468
- # :where :: A filter expression, used to setup a partial index (if supported).
469
- # :unique :: Create a unique index.
470
- #
471
- # PostgreSQL specific options:
472
- #
473
- # :concurrently :: Create the index concurrently, so it doesn't require an exclusive lock
474
- # on the table.
475
- # :index_type :: The underlying index type to use for a full_text index, gin by default).
476
- # :language :: The language to use for a full text index (simple by default).
477
- # :opclass :: Set an opclass to use for all columns (per-column opclasses require
478
- # custom SQL).
479
- # :type :: Set the index type (e.g. full_text, spatial, hash, gin, gist, btree).
480
- # :if_not_exists :: Only create the index if an index of the same name doesn't already exists
481
- #
482
- # MySQL specific options:
483
- #
484
- # :type :: Set the index type, with full_text and spatial indexes handled specially.
485
- #
486
- # Microsoft SQL Server specific options:
487
- #
488
- # :include :: Includes additional columns in the index.
489
- # :key_index :: Sets the KEY INDEX to the given value.
490
- # :type :: clustered uses a clustered index, full_text uses a full text index.
491
470
  def add_index(columns, opts = OPTS)
492
471
  @operations << {:op => :add_index, :columns => Array(columns)}.merge!(opts)
493
472
  nil
@@ -63,7 +63,7 @@ module Sequel
63
63
  # definitions using <tt>create_table</tt>, and +add_index+ accepts all the options
64
64
  # available for index definition.
65
65
  #
66
- # See <tt>Schema::AlterTableGenerator</tt> and the {"Migrations and Schema Modification" guide}[rdoc-ref:doc/migration.rdoc].
66
+ # See <tt>Schema::AlterTableGenerator</tt> and the {Migrations guide}[rdoc-ref:doc/migration.rdoc].
67
67
  def alter_table(name, &block)
68
68
  generator = alter_table_generator(&block)
69
69
  remove_cached_schema(name)
@@ -699,7 +699,7 @@ module Sequel
699
699
  end
700
700
 
701
701
  # Returns a copy of the dataset with a specified order. Can be safely combined with limit.
702
- # If you call limit with an offset, it will override override the offset if you've called
702
+ # If you call limit with an offset, it will override the offset if you've called
703
703
  # offset first.
704
704
  #
705
705
  # DB[:items].offset(10) # SELECT * FROM items OFFSET 10
@@ -1062,10 +1062,8 @@ module Sequel
1062
1062
  # Options:
1063
1063
  # :args :: Specify the arguments/columns for the CTE, should be an array of symbols.
1064
1064
  # :recursive :: Specify that this is a recursive CTE
1065
- #
1066
- # PostgreSQL Specific Options:
1067
1065
  # :materialized :: Set to false to force inlining of the CTE, or true to force not inlining
1068
- # the CTE (PostgreSQL 12+).
1066
+ # the CTE (PostgreSQL 12+/SQLite 3.35+).
1069
1067
  #
1070
1068
  # DB[:items].with(:items, DB[:syx].where(Sequel[:name].like('A%')))
1071
1069
  # # WITH items AS (SELECT * FROM syx WHERE (name LIKE 'A%' ESCAPE '\')) SELECT * FROM items
@@ -1567,6 +1567,13 @@ module Sequel
1567
1567
  sql << ')'
1568
1568
  end
1569
1569
  sql << ' AS '
1570
+
1571
+ case w[:materialized]
1572
+ when true
1573
+ sql << "MATERIALIZED "
1574
+ when false
1575
+ sql << "NOT MATERIALIZED "
1576
+ end
1570
1577
  end
1571
1578
 
1572
1579
  # Whether the symbol cache should be skipped when literalizing the dataset
@@ -8,9 +8,10 @@
8
8
  # DB.extension :date_arithmetic
9
9
  #
10
10
  # Then you can use the Sequel.date_add and Sequel.date_sub methods
11
- # to return Sequel expressions:
11
+ # to return Sequel expressions (this example shows the only supported
12
+ # keys for the second argument):
12
13
  #
13
- # add = Sequel.date_add(:date_column, years: 1, months: 2, days: 3)
14
+ # add = Sequel.date_add(:date_column, years: 1, months: 2, weeks: 2, days: 1)
14
15
  # sub = Sequel.date_sub(:date_column, hours: 1, minutes: 2, seconds: 3)
15
16
  #
16
17
  # In addition to specifying the interval as a hash, there is also
@@ -184,22 +185,35 @@ module Sequel
184
185
  # ActiveSupport::Duration :: Converted to a hash using the interval's parts.
185
186
  def initialize(expr, interval, opts=OPTS)
186
187
  @expr = expr
187
- @interval = if interval.is_a?(Hash)
188
- interval.each_value do |v|
189
- # Attempt to prevent SQL injection by users who pass untrusted strings
190
- # as interval values.
191
- if v.is_a?(String) && !v.is_a?(LiteralString)
192
- raise Sequel::InvalidValue, "cannot provide String value as interval part: #{v.inspect}"
193
- end
188
+
189
+ h = Hash.new(0)
190
+ interval = interval.parts unless interval.is_a?(Hash)
191
+ interval.each do |unit, value|
192
+ # skip nil values
193
+ next unless value
194
+
195
+ # Convert weeks to days, as ActiveSupport::Duration can use weeks,
196
+ # but the database-specific literalizers only support days.
197
+ if unit == :weeks
198
+ unit = :days
199
+ value *= 7
200
+ end
201
+
202
+ unless DatasetMethods::DURATION_UNITS.include?(unit)
203
+ raise Sequel::Error, "Invalid key used in DateAdd interval hash: #{unit.inspect}"
194
204
  end
195
- Hash[interval]
196
- else
197
- h = Hash.new(0)
198
- interval.parts.each{|unit, value| h[unit] += value}
199
- Hash[h]
205
+
206
+ # Attempt to prevent SQL injection by users who pass untrusted strings
207
+ # as interval values. It doesn't make sense to support literal strings,
208
+ # due to the numeric adding below.
209
+ if value.is_a?(String)
210
+ raise Sequel::InvalidValue, "cannot provide String value as interval part: #{value.inspect}"
211
+ end
212
+
213
+ h[unit] += value
200
214
  end
201
215
 
202
- @interval.freeze
216
+ @interval = Hash[h].freeze
203
217
  @cast_type = opts[:cast] if opts[:cast]
204
218
  freeze
205
219
  end