sequel 5.43.0 → 5.47.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 (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