sequel 5.3.0 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG +30 -0
  3. data/bin/sequel +13 -0
  4. data/doc/cheat_sheet.rdoc +1 -0
  5. data/doc/dataset_filtering.rdoc +1 -1
  6. data/doc/querying.rdoc +8 -11
  7. data/doc/release_notes/5.4.0.txt +80 -0
  8. data/doc/testing.rdoc +2 -0
  9. data/lib/sequel/adapters/shared/db2.rb +6 -5
  10. data/lib/sequel/adapters/shared/mssql.rb +5 -8
  11. data/lib/sequel/adapters/shared/mysql.rb +4 -8
  12. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  13. data/lib/sequel/adapters/shared/postgres.rb +5 -3
  14. data/lib/sequel/adapters/shared/sqlanywhere.rb +1 -6
  15. data/lib/sequel/adapters/shared/sqlite.rb +2 -0
  16. data/lib/sequel/database/connecting.rb +1 -1
  17. data/lib/sequel/database/schema_methods.rb +10 -1
  18. data/lib/sequel/dataset/query.rb +1 -2
  19. data/lib/sequel/extensions/date_arithmetic.rb +27 -10
  20. data/lib/sequel/extensions/datetime_parse_to_time.rb +37 -0
  21. data/lib/sequel/extensions/index_caching.rb +107 -0
  22. data/lib/sequel/extensions/null_dataset.rb +3 -1
  23. data/lib/sequel/extensions/pg_timestamptz.rb +26 -0
  24. data/lib/sequel/model/base.rb +2 -2
  25. data/lib/sequel/plugins/class_table_inheritance.rb +11 -3
  26. data/lib/sequel/plugins/json_serializer.rb +2 -2
  27. data/lib/sequel/plugins/xml_serializer.rb +1 -1
  28. data/lib/sequel/version.rb +1 -1
  29. data/spec/adapters/postgres_spec.rb +1 -1
  30. data/spec/adapters/spec_helper.rb +3 -0
  31. data/spec/adapters/sqlite_spec.rb +1 -1
  32. data/spec/bin_spec.rb +9 -0
  33. data/spec/core/connection_pool_spec.rb +2 -2
  34. data/spec/core/dataset_spec.rb +1 -6
  35. data/spec/extensions/class_table_inheritance_spec.rb +52 -2
  36. data/spec/extensions/date_arithmetic_spec.rb +15 -1
  37. data/spec/extensions/datetime_parse_to_time_spec.rb +169 -0
  38. data/spec/extensions/index_caching_spec.rb +66 -0
  39. data/spec/extensions/json_serializer_spec.rb +5 -0
  40. data/spec/extensions/null_dataset_spec.rb +5 -0
  41. data/spec/extensions/pg_extended_date_support_spec.rb +4 -0
  42. data/spec/extensions/pg_timestamptz_spec.rb +17 -0
  43. data/spec/extensions/xml_serializer_spec.rb +7 -0
  44. data/spec/integration/dataset_test.rb +6 -0
  45. data/spec/integration/prepared_statement_test.rb +1 -1
  46. data/spec/integration/schema_test.rb +19 -17
  47. data/spec/integration/spec_helper.rb +4 -0
  48. data/spec/model/record_spec.rb +28 -0
  49. metadata +11 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ac284f3deba98d7ef4b7213b3f6d17c9df344333
4
- data.tar.gz: 3396c7263e758fa01b6820cd1eed79e9f1676be0
2
+ SHA256:
3
+ metadata.gz: 31c759888ccdeebe39b3d3bd9e953c74d8584bade28bdc1bc2d6e38545c8896d
4
+ data.tar.gz: dbf22a0e145192f4a50ea421a4e93a8b182472c5430d06061d710f218bd61d24
5
5
  SHA512:
6
- metadata.gz: c0c7a62b97b76a7a10a8ebe6df53c17de99efb70a2403a18362a32bb11a66b736885430911e982a834d60013ba84c154ebb7609c0b1d7d8c103fe8d4000cb549
7
- data.tar.gz: 9cffa3dd8f2c87aec5598a5ac14676d061c6214f24f1851f721952c3b9c90a263dd2c4d220dfc828f87181faafe9a4d61fe7daf3d82861db0f752871450611f6
6
+ metadata.gz: 764c3d3d8856d8be93d5ee7492e331b1a56caeef4a2ad2d8a53a6c781e34650007f44a1ea0d3cb97b03ebf633842da3c67846edcfe0a0a9c7f788e15123403a9
7
+ data.tar.gz: f0f32cc6e140e46a7cf91f78ace601a0c5bf7c77b87f2fcbfd236fca04090e2bc97e89f579206bb10b9bce84a340215aafe029c9923aec816f33f60aa54da80d
data/CHANGELOG CHANGED
@@ -1,3 +1,33 @@
1
+ === 5.4.0 (2018-01-04)
2
+
3
+ * Enable fractional seconds in timestamps on DB2 (jeremyevans) (#1463)
4
+
5
+ * Don't attempt to insert a second time if insert_select runs a query that doesn't return results, which can happen when triggers are used (jeremyevans)
6
+
7
+ * Make Dataset#insert_select on PostgreSQL and MSSQL return false instead of nil if the INSERT query is sent to the database but returns no rows (jeremyevans)
8
+
9
+ * Add index_caching extension for caching calls to Database#indexes (kenaniah, jeremyevans) (#1461)
10
+
11
+ * Allow Database#indexes on SQLite, MSSQL, SQLAnywhere, and DB2 to handle SQL::Identifier values (jeremyevans)
12
+
13
+ * Add pg_timestamptz extension for using timestamptz (timestamp with time zone) as the default timestamp type (jeremyevans)
14
+
15
+ * Support Sequel.date_{add,sub} :cast option for setting cast type in date_arithmetic extension (jeremyevans)
16
+
17
+ * Optimize Database#synchronize implementation on ruby 2.5+ (jeremyevans)
18
+
19
+ * Add class_table_inheritance plugin :ignore_subclass_columns option (brianphillips) (#1459)
20
+
21
+ * Make Dataset#to_xml in xml_serializer work with eager_graphed datasets (jeremyevans)
22
+
23
+ * Make Dataset#to_json in json_serializer work with eager_graphed datasets (jeremyevans)
24
+
25
+ * Cache Dataset#nullify dataset in the null_dataset extension (chanks) (#1456)
26
+
27
+ * Add datetime_parse_to_time extension, for parsing timestamp strings without offsets using DateTime.parse.to_time (jeremyevans) (#1455)
28
+
29
+ * Add WHERE NULL filter for Dataset#where calls with no existing filter, no argument, and where the virtual row block returns nil (jeremyevans)
30
+
1
31
  === 5.3.0 (2017-12-01)
2
32
 
3
33
  * Add logger to Database instance before making first connection in bin/sequel (jeremyevans)
data/bin/sequel CHANGED
@@ -7,6 +7,7 @@ code = nil
7
7
  copy_databases = nil
8
8
  dump_migration = nil
9
9
  dump_schema = nil
10
+ dump_indexes = nil
10
11
  env = nil
11
12
  migrate_dir = nil
12
13
  migrate_ver = nil
@@ -106,6 +107,11 @@ options = OptionParser.new do |opts|
106
107
  opts.on_tail("-v", "--version", "Show version") do
107
108
  show_version = true
108
109
  end
110
+
111
+ opts.on("-X", "--dump-indexes filename", "dump the index cache for all tables to the file") do |v|
112
+ dump_indexes = v
113
+ exclusive_options << :X
114
+ end
109
115
  end
110
116
  opts = options
111
117
  opts.parse!
@@ -170,6 +176,13 @@ begin
170
176
  DB.dump_schema_cache(dump_schema)
171
177
  exit
172
178
  end
179
+ if dump_indexes
180
+ extra_proc.call
181
+ DB.extension :index_caching
182
+ DB.tables.each{|t| DB.indexes(Sequel::SQL::Identifier.new(t))}
183
+ DB.dump_index_cache(dump_indexes)
184
+ exit
185
+ end
173
186
  if copy_databases
174
187
  Sequel.extension :migration
175
188
  DB.extension :schema_dumper
data/doc/cheat_sheet.rdoc CHANGED
@@ -49,6 +49,7 @@ Without a filename argument, the sqlite adapter will setup a new sqlite database
49
49
  dataset.each{|r| p r}
50
50
  dataset.all # => [{...}, {...}, ...]
51
51
  dataset.first # => {...}
52
+ dataset.last # => {...}
52
53
 
53
54
  == Update/Delete rows
54
55
 
@@ -84,7 +84,7 @@ You can also use the =~ operator:
84
84
  This works with other hash values, such as arrays and ranges:
85
85
 
86
86
  items.where{Sequel.|({category: ['ruby', 'other']}, (price - 100 > 200))}.sql
87
- # "SELECT * FROM items WHERE ((category IN ('ruby', 'other')) OR ((price - 100) <= 200))"
87
+ # "SELECT * FROM items WHERE ((category IN ('ruby', 'other')) OR ((price - 100) > 200))"
88
88
 
89
89
  items.where{(price =~ (100..200)) & :active}.sql
90
90
  # "SELECT * FROM items WHERE ((price >= 100 AND price <= 200) AND active)"
data/doc/querying.rdoc CHANGED
@@ -49,7 +49,7 @@ to raise an exception if no record is found, you can use <tt>Sequel::Model.with_
49
49
 
50
50
  ==== Using +first+
51
51
 
52
- If you just want the first record in the dataset,
52
+ If you want the first record in the dataset,
53
53
  <tt>Sequel::Dataset#first</tt> is probably the most obvious method to use:
54
54
 
55
55
  artist = Artist.first
@@ -82,22 +82,19 @@ Dataset#[] does not (unless it is a model dataset).
82
82
  ==== Using +last+
83
83
 
84
84
  If you want the last record in the dataset,
85
- <tt>Sequel::Dataset#last</tt> is an obvious method to use. Note
86
- that last requires that the dataset be ordered, unless the
87
- dataset is a model dataset. For a model dataset, +last+ will do a
88
- reverse order by the primary key field:
85
+ <tt>Sequel::Dataset#last</tt> is an obvious method to use. +last+ requires the
86
+ dataset be ordered, unless the dataset is a model dataset in which case +last+
87
+ will do a reverse order by the primary key field:
89
88
 
90
89
  artist = Artist.last
91
90
  # SELECT * FROM artists ORDER BY id DESC LIMIT 1
92
91
  # => #<Artist @values={:name=>"YJM", :id=>1}>
93
92
 
94
- Note that what +last+ does is reverse the order of the dataset and then
95
- call +first+. This is why +last+ raises a Sequel::Error if there is no
96
- order on a plain dataset, because otherwise it would provide the same record
97
- as +first+, and most users would find that confusing.
93
+ Note:
98
94
 
99
- Note that +last+ is not necessarily going to give you the last record
100
- in the dataset unless you give the dataset an unambiguous order.
95
+ 1. +last+ is equivalent to running a +reverse.first+ query, in other words it reverses the order of the dataset and then calls +first+. This is why +last+ raises a Sequel::Error when there is no order on a plain dataset - because it will provide the same record as +first+, and most users will find that confusing.
96
+ 2. +last+ is not necessarily going to give you the last record in the dataset unless you give the dataset an unambiguous order.
97
+ 3. +last+ will ignore +limit+ if chained together in a query because it sets a limit of 1 if no arguments are given.
101
98
 
102
99
  ==== Retrieving a Single Column Value
103
100
 
@@ -0,0 +1,80 @@
1
+ = New Features
2
+
3
+ * An index_caching extension has been added, which makes
4
+ Database#indexes use a cache similar to Database#schema, and also
5
+ offers methods for saving and loading the cache from a file, similar
6
+ to the schema_caching extension.
7
+
8
+ This can speed up model loaded in certain cases when the
9
+ auto_validations plugin is used.
10
+
11
+ * A datetime_parse_to_time extension has been added, which parses
12
+ strings without timezone offsets using DateTime.parse intead of
13
+ Time.parse. This can fix problems when the string being parsed
14
+ represents a time not valid in the local timezone due to daylight
15
+ savings time shifts. Time.parse silently shifts such times by 1
16
+ hour instead of raising an exception, resulting in incorrect
17
+ behavior in that case.
18
+
19
+ It only makes sense to use this extension when the times in the
20
+ database are stored in UTC but not returned with timezone
21
+ information, the timezone for the Database instance
22
+ (or Sequel.database_timezone) is set to :utc (not the default),
23
+ and Time is used as the datetime_class (the default).
24
+
25
+ * A pg_timestamptz extension has been added for switching the default
26
+ generic timestamp type from timestamp to timestamptz.
27
+
28
+ * Sequel.date_{add,sub} in the date_arithmetic extension now supports
29
+ a :cast option for setting the cast type. This value defaults to
30
+ Time for backwards compatibility, which uses the default generic
31
+ timestamp type for the database.
32
+
33
+ * The class_table_inheritance plugin now supports an
34
+ :ignore_subclass_columns option which takes an array of column
35
+ symbols to ignore in subclasses. This allows you to use
36
+ the plugin when your table inheritance hierarchy includes
37
+ non-primary key columns with the same name in different tables.
38
+
39
+ = Improvements
40
+
41
+ * Dataset#insert_select now returns false instead of nil if it runs
42
+ an INSERT statement but does not return a value on Microsoft SQL
43
+ Server or PostgreSQL. This can happen on both databases if triggers
44
+ are used.
45
+
46
+ Model#save now checks for a false value returned by
47
+ Dataset#insert_select, and does not issue another INSERT statement
48
+ in that case.
49
+
50
+ * Database#indexes now correctly handles SQL::Identifier arguments on
51
+ SQLite, Microsoft SQL Server, SQLAnywhere, and DB2.
52
+
53
+ * Dataset#to_json in the json_serializer plugin and Dataset#to_xml
54
+ in the xml_serializer plugin now both handle datasets that use
55
+ eager_graph.
56
+
57
+ * Dataset#nullify now caches the dataset it returns, for better
58
+ performance if it is called more than once on the same dataset.
59
+
60
+ * Database#synchronize is now optimized on ruby 2.5+ and is about
61
+ 10% faster by relying on the new lazy proc allocation feature.
62
+
63
+ = Backwards Compatibility
64
+
65
+ * Fractional second timestamps are now enabled on DB2. If you are
66
+ connecting to a DB2 database that does not support fractional
67
+ seconds, you should add the following code (where DB is your
68
+ Sequel::Database instance):
69
+
70
+ DB.extend_datasets do
71
+ def supports_timestamp_usecs?
72
+ false
73
+ end
74
+ end
75
+
76
+ * Calling a filtering method with no argument and a virtual row
77
+ block that returns nil on a dataset with no existing filter now
78
+ adds a WHERE NULL filter, to match the behavior if given a nil
79
+ argument. Previously, a deprecation warning was issued and a
80
+ dataset with no filter was returned.
data/doc/testing.rdoc CHANGED
@@ -156,6 +156,7 @@ SEQUEL_COLUMNS_INTROSPECTION :: Use the columns_introspection extension when run
156
156
  SEQUEL_CONNECTION_VALIDATOR :: Use the connection validator extension when running the specs
157
157
  SEQUEL_DUPLICATE_COLUMNS_HANDLER :: Use the duplicate columns handler extension with value given when running the specs
158
158
  SEQUEL_ERROR_SQL :: Use the error_sql extension when running the specs
159
+ SEQUEL_INDEX_CACHING :: Use the index_caching extension when running the specs
159
160
  SEQUEL_FREEZE_DATABASE :: Freeze the database before running the integration specs
160
161
  SEQUEL_IDENTIFIER_MANGLING :: Use the identifier_mangling extension when running the specs
161
162
  SEQUEL_MODEL_PREPARED_STATEMENTS :: Use the prepared_statements plugin when running the specs
@@ -163,5 +164,6 @@ SEQUEL_NO_CACHE_ASSOCIATIONS :: Don't cache association metadata when running th
163
164
  SEQUEL_NO_CHECK_SQLS :: Don't check for specific SQL syntax when running the specs
164
165
  SEQUEL_CHECK_PENDING :: Try running all specs (note, can cause lockups for some adapters), and raise errors for skipped specs that don't fail
165
166
  SEQUEL_NO_PENDING :: Don't skip any specs, try running all specs (note, can cause lockups for some adapters)
167
+ SEQUEL_PG_TIMESTAMPTZ :: Use the pg_timestamptz extension when running the postgres specs
166
168
  SEQUEL_SPLIT_SYMBOLS :: Turn on symbol splitting when running the adapter and integration specs
167
169
  SEQUEL_SYNCHRONIZE_SQL :: Use the synchronize_sql extension when running the specs
@@ -69,6 +69,7 @@ module Sequel
69
69
  # Use SYSCAT.INDEXES to get the indexes for the table
70
70
  def indexes(table, opts = OPTS)
71
71
  m = output_identifier_meth
72
+ table = table.value if table.is_a?(Sequel::SQL::Identifier)
72
73
  indexes = {}
73
74
  metadata_dataset.
74
75
  from(Sequel[:syscat][:indexes]).
@@ -345,11 +346,6 @@ module Sequel
345
346
  false
346
347
  end
347
348
 
348
- # DB2 does not support fractional seconds in timestamps.
349
- def supports_timestamp_usecs?
350
- false
351
- end
352
-
353
349
  # DB2 supports window functions
354
350
  def supports_window_functions?
355
351
  true
@@ -384,6 +380,11 @@ module Sequel
384
380
  '0'
385
381
  end
386
382
 
383
+ # DB2 doesn't support fractional seconds in times, only fractional seconds in timestamps.
384
+ def literal_sqltime(v)
385
+ v.strftime("'%H:%M:%S'")
386
+ end
387
+
387
388
  # Use 1 for true on DB2
388
389
  def literal_true
389
390
  '1'
@@ -165,6 +165,7 @@ module Sequel
165
165
  m = output_identifier_meth
166
166
  im = input_identifier_meth
167
167
  indexes = {}
168
+ table = table.value if table.is_a?(Sequel::SQL::Identifier)
168
169
  i = Sequel[:i]
169
170
  ds = metadata_dataset.from(Sequel.lit('[sys].[tables]').as(:t)).
170
171
  join(Sequel.lit('[sys].[indexes]').as(:i), :object_id=>:object_id).
@@ -473,12 +474,6 @@ module Sequel
473
474
  :datetime
474
475
  end
475
476
 
476
- # MSSQL has both datetime and timestamp classes, most people are going
477
- # to want datetime
478
- def type_literal_generic_time(column)
479
- column[:only_time] ? :time : :datetime
480
- end
481
-
482
477
  # MSSQL doesn't have a true boolean class, so it uses bit
483
478
  def type_literal_generic_trueclass(column)
484
479
  :bit
@@ -580,10 +575,12 @@ module Sequel
580
575
  where(Sequel.lit("CONTAINS (?, ?)", cols, terms))
581
576
  end
582
577
 
583
- # Use the OUTPUT clause to get the value of all columns for the newly inserted record.
578
+ # Insert a record, returning the record inserted, using OUTPUT. Always returns nil without
579
+ # running an INSERT statement if disable_insert_output is used. If the query runs
580
+ # but returns no values, returns false.
584
581
  def insert_select(*values)
585
582
  return unless supports_insert_select?
586
- with_sql_first(insert_select_sql(*values))
583
+ with_sql_first(insert_select_sql(*values)) || false
587
584
  end
588
585
 
589
586
  # Add OUTPUT clause unless there is already an existing output clause, then return
@@ -537,15 +537,11 @@ module Sequel
537
537
 
538
538
  # MySQL has both datetime and timestamp classes, most people are going
539
539
  # to want datetime.
540
- def type_literal_generic_time(column)
541
- if column[:only_time]
542
- if supports_timestamp_usecs?
543
- :'time(6)'
544
- else
545
- :time
546
- end
540
+ def type_literal_generic_only_time(column)
541
+ if supports_timestamp_usecs?
542
+ :'time(6)'
547
543
  else
548
- type_literal_generic_datetime(column)
544
+ :time
549
545
  end
550
546
  end
551
547
 
@@ -285,7 +285,7 @@ module Sequel
285
285
 
286
286
  # Oracle doesn't have a time type, so use timestamp for all
287
287
  # time columns.
288
- def type_literal_generic_time(column)
288
+ def type_literal_generic_only_time(column)
289
289
  :timestamp
290
290
  end
291
291
 
@@ -1393,11 +1393,13 @@ module Sequel
1393
1393
  insert_conflict
1394
1394
  end
1395
1395
 
1396
- # Insert a record returning the record inserted. Always returns nil without
1397
- # inserting a query if disable_insert_returning is used.
1396
+ # Insert a record, returning the record inserted, using RETURNING. Always returns nil without
1397
+ # running an INSERT statement if disable_insert_returning is used. If the query runs
1398
+ # but returns no values, returns false.
1398
1399
  def insert_select(*values)
1399
1400
  return unless supports_insert_select?
1400
- server?(:default).with_sql_first(insert_select_sql(*values))
1401
+ # Handle case where query does not return a row
1402
+ server?(:default).with_sql_first(insert_select_sql(*values)) || false
1401
1403
  end
1402
1404
 
1403
1405
  # The SQL to use for an insert_select, adds a RETURNING clause to the insert
@@ -58,6 +58,7 @@ module Sequel
58
58
  def indexes(table, opts = OPTS)
59
59
  m = output_identifier_meth
60
60
  im = input_identifier_meth
61
+ table = table.value if table.is_a?(Sequel::SQL::Identifier)
61
62
  indexes = {}
62
63
  metadata_dataset.
63
64
  from(Sequel[:dbo][:sysobjects].as(:z)).
@@ -158,12 +159,6 @@ module Sequel
158
159
  :datetime
159
160
  end
160
161
 
161
- # Sybase has both datetime and timestamp classes, most people are going
162
- # to want datetime
163
- def type_literal_generic_time(column)
164
- column[:only_time] ? :time : :datetime
165
- end
166
-
167
162
  # Sybase doesn't have a true boolean class, so it uses integer
168
163
  def type_literal_generic_trueclass(column)
169
164
  :smallint
@@ -82,6 +82,7 @@ module Sequel
82
82
  m = output_identifier_meth
83
83
  im = input_identifier_meth
84
84
  indexes = {}
85
+ table = table.value if table.is_a?(Sequel::SQL::Identifier)
85
86
  metadata_dataset.with_sql("PRAGMA index_list(?)", im.call(table)).each do |r|
86
87
  if opts[:only_autocreated]
87
88
  # If specifically asked for only autocreated indexes, then return those an only those
@@ -188,6 +189,7 @@ module Sequel
188
189
  ops.each{|op| alter_table_sql_list(table, [op]).flatten.each{|sql| execute_ddl(sql)}}
189
190
  end
190
191
  end
192
+ remove_cached_schema(table)
191
193
  ensure
192
194
  run "PRAGMA foreign_keys = 1" if fks
193
195
  end
@@ -247,7 +247,7 @@ module Sequel
247
247
  @single_threaded
248
248
  end
249
249
 
250
- if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
250
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION < '2.5'
251
251
  # Acquires a database connection, yielding it to the passed block. This is
252
252
  # useful if you want to make sure the same connection is used for all
253
253
  # database queries in the block. It is also useful if you want to gain
@@ -1010,7 +1010,16 @@ module Sequel
1010
1010
  # Sequel uses the timestamp type by default for Time values.
1011
1011
  # If the :only_time option is used, the time type is used.
1012
1012
  def type_literal_generic_time(column)
1013
- column[:only_time] ? :time : :timestamp
1013
+ if column[:only_time]
1014
+ type_literal_generic_only_time(column)
1015
+ else
1016
+ type_literal_generic_datetime(column)
1017
+ end
1018
+ end
1019
+
1020
+ # Use time by default for Time values if :only_time option is used.
1021
+ def type_literal_generic_only_time(column)
1022
+ :time
1014
1023
  end
1015
1024
 
1016
1025
  # Sequel uses the boolean type by default for TrueClass and FalseClass.
@@ -1211,8 +1211,7 @@ module Sequel
1211
1211
  cond = SQL::BooleanExpression.new(combine, @opts[clause], cond) if @opts[clause]
1212
1212
 
1213
1213
  if cond.nil?
1214
- Sequel::Deprecation.deprecate('Filtering method called on dataset with no existing filter with virtual row block and no argument and virtual row block returned nil. Currently, this results in the filter being ignored instead of using a NULL filter. In Sequel 5.4+, this behavior will change to using a NULL filter, similar to the behavior in all other cases.')
1215
- #cond = Sequel::NULL
1214
+ cond = Sequel::NULL
1216
1215
  end
1217
1216
 
1218
1217
  clone(clause => cond)
@@ -21,6 +21,11 @@
21
21
  # add = Sequel.date_add(:date_column, 1.years + 2.months + 3.days)
22
22
  # sub = Sequel.date_sub(:date_column, 1.hours + 2.minutes + 3.seconds)
23
23
  #
24
+ # By default, values are casted to the generic timestamp type for the
25
+ # database. You can override the cast type using the :cast option:
26
+ #
27
+ # add = Sequel.date_add(:date_column, {years: 1, months: 2, days: 3}, :cast=>:timestamptz)
28
+ #
24
29
  # These expressions can be used in your datasets, or anywhere else that
25
30
  # Sequel expressions are allowed:
26
31
  #
@@ -33,13 +38,17 @@ module Sequel
33
38
  module SQL
34
39
  module Builders
35
40
  # Return a DateAdd expression, adding an interval to the date/timestamp expr.
36
- def date_add(expr, interval)
37
- DateAdd.new(expr, interval)
41
+ # Options:
42
+ # :cast :: Cast to the specified type instead of the default if casting
43
+ def date_add(expr, interval, opts=OPTS)
44
+ DateAdd.new(expr, interval, opts)
38
45
  end
39
46
 
40
47
  # Return a DateAdd expression, adding the negative of the interval to
41
48
  # the date/timestamp expr.
42
- def date_sub(expr, interval)
49
+ # Options:
50
+ # :cast :: Cast to the specified type instead of the default if casting
51
+ def date_sub(expr, interval, opts=OPTS)
43
52
  interval = if interval.is_a?(Hash)
44
53
  h = {}
45
54
  interval.each{|k,v| h[k] = -v unless v.nil?}
@@ -47,7 +56,7 @@ module Sequel
47
56
  else
48
57
  -interval
49
58
  end
50
- DateAdd.new(expr, interval)
59
+ DateAdd.new(expr, interval, opts)
51
60
  end
52
61
  end
53
62
 
@@ -72,8 +81,11 @@ module Sequel
72
81
  if defined?(super)
73
82
  return super
74
83
  end
84
+
75
85
  h = da.interval
76
86
  expr = da.expr
87
+ cast_type = da.cast_type || Time
88
+
77
89
  cast = case db_type = db.database_type
78
90
  when :postgres
79
91
  interval = String.new
@@ -81,9 +93,9 @@ module Sequel
81
93
  interval << "#{value} #{sql_unit} "
82
94
  end
83
95
  if interval.empty?
84
- return literal_append(sql, Sequel.cast(expr, Time))
96
+ return literal_append(sql, Sequel.cast(expr, cast_type))
85
97
  else
86
- return complex_expression_sql_append(sql, :+, [Sequel.cast(expr, Time), Sequel.cast(interval, :interval)])
98
+ return complex_expression_sql_append(sql, :+, [Sequel.cast(expr, cast_type), Sequel.cast(interval, :interval)])
87
99
  end
88
100
  when :sqlite
89
101
  args = [expr]
@@ -94,7 +106,7 @@ module Sequel
94
106
  when :mysql, :hsqldb
95
107
  if db_type == :hsqldb
96
108
  # HSQLDB requires 2.2.9+ for the DATE_ADD function
97
- expr = Sequel.cast(expr, Time)
109
+ expr = Sequel.cast(expr, cast_type)
98
110
  end
99
111
  each_valid_interval_unit(h, MYSQL_DURATION_UNITS) do |value, sql_unit|
100
112
  expr = Sequel.function(:DATE_ADD, expr, Sequel.lit(["INTERVAL ", " "], value, sql_unit))
@@ -124,7 +136,7 @@ module Sequel
124
136
  expr = Sequel.+(expr, Sequel.lit(["INTERVAL ", " "], value.to_s, sql_unit))
125
137
  end
126
138
  when :db2
127
- expr = Sequel.cast(expr, Time)
139
+ expr = Sequel.cast(expr, cast_type)
128
140
  each_valid_interval_unit(h, DB2_DURATION_UNITS) do |value, sql_unit|
129
141
  expr = Sequel.+(expr, Sequel.lit(["", " "], value, sql_unit))
130
142
  end
@@ -134,7 +146,7 @@ module Sequel
134
146
  end
135
147
 
136
148
  if cast
137
- expr = Sequel.cast(expr, Time)
149
+ expr = Sequel.cast(expr, cast_type)
138
150
  end
139
151
 
140
152
  literal_append(sql, expr)
@@ -165,10 +177,14 @@ module Sequel
165
177
  # symbol keys.
166
178
  attr_reader :interval
167
179
 
180
+ # The type to cast the expression to. nil if not overridden, in which cast
181
+ # the generic timestamp type for the database will be used.
182
+ attr_reader :cast_type
183
+
168
184
  # Supports two types of intervals:
169
185
  # Hash :: Used directly, but values cannot be plain strings.
170
186
  # ActiveSupport::Duration :: Converted to a hash using the interval's parts.
171
- def initialize(expr, interval)
187
+ def initialize(expr, interval, opts=OPTS)
172
188
  @expr = expr
173
189
  @interval = if interval.is_a?(Hash)
174
190
  interval.each_value do |v|
@@ -186,6 +202,7 @@ module Sequel
186
202
  end
187
203
 
188
204
  @interval.freeze
205
+ @cast_type = opts[:cast] if opts[:cast]
189
206
  freeze
190
207
  end
191
208