sequel 3.3.0 → 3.4.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.
- data/CHANGELOG +62 -0
- data/README.rdoc +4 -4
- data/doc/release_notes/3.3.0.txt +1 -1
- data/doc/release_notes/3.4.0.txt +325 -0
- data/doc/sharding.rdoc +3 -3
- data/lib/sequel/adapters/amalgalite.rb +1 -1
- data/lib/sequel/adapters/firebird.rb +4 -9
- data/lib/sequel/adapters/jdbc.rb +21 -7
- data/lib/sequel/adapters/mysql.rb +2 -1
- data/lib/sequel/adapters/odbc.rb +7 -21
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +6 -1
- data/lib/sequel/adapters/shared/mssql.rb +11 -0
- data/lib/sequel/adapters/shared/mysql.rb +8 -12
- data/lib/sequel/adapters/shared/oracle.rb +13 -0
- data/lib/sequel/adapters/shared/postgres.rb +5 -10
- data/lib/sequel/adapters/shared/sqlite.rb +21 -1
- data/lib/sequel/adapters/sqlite.rb +2 -2
- data/lib/sequel/core.rb +147 -11
- data/lib/sequel/database.rb +21 -9
- data/lib/sequel/dataset.rb +31 -6
- data/lib/sequel/dataset/convenience.rb +1 -1
- data/lib/sequel/dataset/sql.rb +76 -18
- data/lib/sequel/extensions/inflector.rb +2 -51
- data/lib/sequel/model.rb +16 -10
- data/lib/sequel/model/associations.rb +4 -1
- data/lib/sequel/model/base.rb +13 -6
- data/lib/sequel/model/default_inflections.rb +46 -0
- data/lib/sequel/model/inflections.rb +1 -51
- data/lib/sequel/plugins/boolean_readers.rb +52 -0
- data/lib/sequel/plugins/instance_hooks.rb +57 -0
- data/lib/sequel/plugins/lazy_attributes.rb +13 -1
- data/lib/sequel/plugins/nested_attributes.rb +171 -0
- data/lib/sequel/plugins/serialization.rb +35 -16
- data/lib/sequel/plugins/timestamps.rb +87 -0
- data/lib/sequel/plugins/validation_helpers.rb +8 -1
- data/lib/sequel/sql.rb +33 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/sqlite_spec.rb +11 -6
- data/spec/core/core_sql_spec.rb +29 -0
- data/spec/core/database_spec.rb +16 -7
- data/spec/core/dataset_spec.rb +264 -20
- data/spec/extensions/boolean_readers_spec.rb +86 -0
- data/spec/extensions/inflector_spec.rb +67 -4
- data/spec/extensions/instance_hooks_spec.rb +133 -0
- data/spec/extensions/lazy_attributes_spec.rb +45 -5
- data/spec/extensions/nested_attributes_spec.rb +272 -0
- data/spec/extensions/serialization_spec.rb +64 -1
- data/spec/extensions/timestamps_spec.rb +150 -0
- data/spec/extensions/validation_helpers_spec.rb +18 -0
- data/spec/integration/dataset_test.rb +79 -2
- data/spec/integration/schema_test.rb +17 -0
- data/spec/integration/timezone_test.rb +55 -0
- data/spec/model/associations_spec.rb +19 -7
- data/spec/model/model_spec.rb +29 -0
- data/spec/model/record_spec.rb +36 -0
- data/spec/spec_config.rb +1 -1
- metadata +14 -2
@@ -32,10 +32,10 @@ module Sequel
|
|
32
32
|
db.busy_timeout(opts.fetch(:timeout, 5000))
|
33
33
|
db.type_translation = true
|
34
34
|
|
35
|
-
# Handle
|
35
|
+
# Handle datetimes with Sequel.datetime_class
|
36
36
|
prok = proc do |t,v|
|
37
37
|
v = Time.at(v.to_i).iso8601 if UNIX_EPOCH_TIME_FORMAT.match(v)
|
38
|
-
Sequel.
|
38
|
+
Sequel.database_to_application_timestamp(v)
|
39
39
|
end
|
40
40
|
db.translator.add_translator("timestamp", &prok)
|
41
41
|
db.translator.add_translator("datetime", &prok)
|
data/lib/sequel/core.rb
CHANGED
@@ -29,15 +29,58 @@
|
|
29
29
|
#
|
30
30
|
# Sequel.datetime_class = DateTime
|
31
31
|
#
|
32
|
+
# Sequel doesn't pay much attention to timezones by default, but you can set it
|
33
|
+
# handle timezones if you want. There are three separate timezone settings:
|
34
|
+
#
|
35
|
+
# * application_timezone - The timezone you want the application to use. This is
|
36
|
+
# the timezone that incoming times from the database and typecasting are converted
|
37
|
+
# to.
|
38
|
+
# * database_timezone - The timezone for storage in the database. This is the
|
39
|
+
# timezone to which Sequel will convert timestamps before literalizing them
|
40
|
+
# for storage in the database. It is also the timezone that Sequel will assume
|
41
|
+
# database timestamp values are already in (if they don't include an offset).
|
42
|
+
# * typecast_timezone - The timezone that incoming data that Sequel needs to typecast
|
43
|
+
# is assumed to be already in (if they don't include an offset).
|
44
|
+
#
|
45
|
+
# You can set also set all three timezones to the same value at once via
|
46
|
+
# Sequel.default_timezone=.
|
47
|
+
#
|
48
|
+
# Sequel.application_timezone = :utc # or :local or nil
|
49
|
+
# Sequel.database_timezone = :utc # or :local or nil
|
50
|
+
# Sequel.typecast_timezone = :utc # or :local or nil
|
51
|
+
# Sequel.default_timezone = :utc # or :local or nil
|
52
|
+
#
|
53
|
+
# The only timezone values that are supported by default are :utc (convert to UTC),
|
54
|
+
# :local (convert to local time), and nil (don't convert). If you need to
|
55
|
+
# convert to a specific timezone, or need the timezones being used to change based
|
56
|
+
# on the environment (e.g. current user), you need to use an extension (and use
|
57
|
+
# DateTime as the datetime_class).
|
58
|
+
#
|
32
59
|
# You can set the SEQUEL_NO_CORE_EXTENSIONS constant or environment variable to have
|
33
60
|
# Sequel not extend the core classes.
|
34
61
|
module Sequel
|
62
|
+
# The offset of the current time zone from UTC, in seconds.
|
63
|
+
LOCAL_DATETIME_OFFSET_SECS = Time.now.utc_offset
|
64
|
+
|
65
|
+
# The offset of the current time zone from UTC, as a fraction of a day.
|
66
|
+
LOCAL_DATETIME_OFFSET = respond_to?(:Rational, true) ? Rational(LOCAL_DATETIME_OFFSET_SECS, 60*60*24) : LOCAL_DATETIME_OFFSET_SECS/60/60/24.0
|
67
|
+
|
68
|
+
@application_timezone = nil
|
35
69
|
@convert_two_digit_years = true
|
70
|
+
@database_timezone = nil
|
36
71
|
@datetime_class = Time
|
72
|
+
@typecast_timezone = nil
|
37
73
|
@virtual_row_instance_eval = true
|
38
74
|
|
39
75
|
class << self
|
40
76
|
attr_accessor :convert_two_digit_years, :datetime_class, :virtual_row_instance_eval
|
77
|
+
attr_accessor :application_timezone, :database_timezone, :typecast_timezone
|
78
|
+
end
|
79
|
+
|
80
|
+
# Convert the given Time/DateTime object into the database timezone, used when
|
81
|
+
# literalizing objects in an SQL string.
|
82
|
+
def self.application_to_database_timestamp(v)
|
83
|
+
convert_output_timestamp(v, Sequel.database_timezone)
|
41
84
|
end
|
42
85
|
|
43
86
|
# Returns true if the passed object could be a specifier of conditions, false otherwise.
|
@@ -73,6 +116,29 @@ module Sequel
|
|
73
116
|
Database.connect(*args, &block)
|
74
117
|
end
|
75
118
|
|
119
|
+
# Convert the given object into an object of Sequel.datetime_class in the
|
120
|
+
# application_timezone. Used when coverting datetime/timestamp columns
|
121
|
+
# returned by the database.
|
122
|
+
def self.database_to_application_timestamp(v)
|
123
|
+
convert_timestamp(v, Sequel.database_timezone)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Sets the database, application, and typecasting timezones to the given timezone.
|
127
|
+
def self.default_timezone=(tz)
|
128
|
+
self.database_timezone = tz
|
129
|
+
self.application_timezone = tz
|
130
|
+
self.typecast_timezone = tz
|
131
|
+
end
|
132
|
+
|
133
|
+
# Load all Sequel extensions given. Only loads extensions included in this
|
134
|
+
# release of Sequel, doesn't load external extensions.
|
135
|
+
#
|
136
|
+
# Sequel.extension(:schema_dumper)
|
137
|
+
# Sequel.extension(:pagination, :query)
|
138
|
+
def self.extension(*extensions)
|
139
|
+
require(extensions, 'extensions')
|
140
|
+
end
|
141
|
+
|
76
142
|
# Set the method to call on identifiers going into the database. This affects
|
77
143
|
# the literalization of identifiers by calling this method on them before they are input.
|
78
144
|
# Sequel upcases identifiers in all SQL strings for most databases, so to turn that off:
|
@@ -112,15 +178,6 @@ module Sequel
|
|
112
178
|
Database.quote_identifiers = value
|
113
179
|
end
|
114
180
|
|
115
|
-
# Load all Sequel extensions given. Only loads extensions included in this
|
116
|
-
# release of Sequel, doesn't load external extensions.
|
117
|
-
#
|
118
|
-
# Sequel.extension(:schema_dumper)
|
119
|
-
# Sequel.extension(:pagination, :query)
|
120
|
-
def self.extension(*extensions)
|
121
|
-
require(extensions, 'extensions')
|
122
|
-
end
|
123
|
-
|
124
181
|
# Require all given files which should be in the same or a subdirectory of
|
125
182
|
# this file. If a subdir is given, assume all files are in that subdir.
|
126
183
|
def self.require(files, subdir=nil)
|
@@ -168,7 +225,14 @@ module Sequel
|
|
168
225
|
raise InvalidValue, "Invalid Time value #{s.inspect} (#{e.message})"
|
169
226
|
end
|
170
227
|
end
|
171
|
-
|
228
|
+
|
229
|
+
# Convert the given object into an object of Sequel.datetime_class in the
|
230
|
+
# application_timezone. Used when typecasting values when assigning them
|
231
|
+
# to model datetime attributes.
|
232
|
+
def self.typecast_to_application_timestamp(v)
|
233
|
+
convert_timestamp(v, Sequel.typecast_timezone)
|
234
|
+
end
|
235
|
+
|
172
236
|
### Private Class Methods ###
|
173
237
|
|
174
238
|
# Helper method that the database adapter class methods that are added to Sequel via
|
@@ -184,6 +248,78 @@ module Sequel
|
|
184
248
|
end
|
185
249
|
connect(opts, &block)
|
186
250
|
end
|
251
|
+
|
252
|
+
# Converts the object from a String, Array, Date, DateTime, or Time into an
|
253
|
+
# instance of Sequel.datetime_class. If a string and an offset is not given,
|
254
|
+
# assume that the string is already in the given input_timezone.
|
255
|
+
def self.convert_input_timestamp(v, input_timezone) # :nodoc:
|
256
|
+
case v
|
257
|
+
when String
|
258
|
+
v2 = Sequel.string_to_datetime(v)
|
259
|
+
if !input_timezone || Date._parse(v).has_key?(:offset)
|
260
|
+
v2
|
261
|
+
else
|
262
|
+
# Correct for potentially wrong offset if offset is given
|
263
|
+
if v2.is_a?(DateTime)
|
264
|
+
# DateTime assumes UTC if no offset is given
|
265
|
+
v2 = v2.new_offset(LOCAL_DATETIME_OFFSET) - LOCAL_DATETIME_OFFSET if input_timezone == :local
|
266
|
+
else
|
267
|
+
# Time assumes local time if no offset is given
|
268
|
+
v2 = v2.getutc + LOCAL_DATETIME_OFFSET_SECS if input_timezone == :utc
|
269
|
+
end
|
270
|
+
v2
|
271
|
+
end
|
272
|
+
when Array
|
273
|
+
y, mo, d, h, mi, s = v
|
274
|
+
if datetime_class == DateTime
|
275
|
+
DateTime.civil(y, mo, d, h, mi, s, input_timezone == :utc ? 0 : LOCAL_DATETIME_OFFSET)
|
276
|
+
else
|
277
|
+
Time.send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s)
|
278
|
+
end
|
279
|
+
when Time
|
280
|
+
if datetime_class == DateTime
|
281
|
+
v.respond_to?(:to_datetime) ? v.to_datetime : string_to_datetime(v.iso8601)
|
282
|
+
else
|
283
|
+
v
|
284
|
+
end
|
285
|
+
when DateTime
|
286
|
+
if datetime_class == DateTime
|
287
|
+
v
|
288
|
+
else
|
289
|
+
v.respond_to?(:to_time) ? v.to_time : string_to_datetime(v.to_s)
|
290
|
+
end
|
291
|
+
when Date
|
292
|
+
convert_input_timestamp(v.to_s, input_timezone)
|
293
|
+
else
|
294
|
+
raise InvalidValue, "Invalid convert_input_timestamp type: #{v.inspect}"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Converts the object to the given output_timezone.
|
299
|
+
def self.convert_output_timestamp(v, output_timezone) # :nodoc:
|
300
|
+
if output_timezone
|
301
|
+
if v.is_a?(DateTime)
|
302
|
+
v.new_offset(output_timezone == :utc ? 0 : LOCAL_DATETIME_OFFSET)
|
303
|
+
else
|
304
|
+
v.send(output_timezone == :utc ? :getutc : :getlocal)
|
305
|
+
end
|
306
|
+
else
|
307
|
+
v
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Converts the given object from the given input timezone to the
|
312
|
+
# application timezone using convert_input_timestamp and
|
313
|
+
# convert_output_timestamp.
|
314
|
+
def self.convert_timestamp(v, input_timezone) # :nodoc:
|
315
|
+
begin
|
316
|
+
convert_output_timestamp(convert_input_timestamp(v, input_timezone), Sequel.application_timezone)
|
317
|
+
rescue InvalidValue
|
318
|
+
raise
|
319
|
+
rescue
|
320
|
+
raise InvalidValue, "Invalid #{datetime_class} value: #{v.inspect}"
|
321
|
+
end
|
322
|
+
end
|
187
323
|
|
188
324
|
# Method that adds a database adapter class method to Sequel that calls
|
189
325
|
# Sequel.adapter_method.
|
@@ -193,7 +329,7 @@ module Sequel
|
|
193
329
|
end
|
194
330
|
end
|
195
331
|
|
196
|
-
private_class_method :adapter_method, :def_adapter_method
|
332
|
+
private_class_method :adapter_method, :convert_input_timestamp, :convert_output_timestamp, :convert_timestamp, :def_adapter_method
|
197
333
|
|
198
334
|
require(%w"metaprogramming sql connection_pool exceptions dataset database version")
|
199
335
|
require(%w"schema_generator schema_methods schema_sql", 'database')
|
data/lib/sequel/database.rb
CHANGED
@@ -234,9 +234,10 @@ module Sequel
|
|
234
234
|
|
235
235
|
### Instance Methods ###
|
236
236
|
|
237
|
-
#
|
237
|
+
# Runs the supplied SQL statement string on the database server.
|
238
|
+
# Alias for run.
|
238
239
|
def <<(sql)
|
239
|
-
|
240
|
+
run(sql)
|
240
241
|
end
|
241
242
|
|
242
243
|
# Returns a dataset from the database. If the first argument is a string,
|
@@ -429,6 +430,14 @@ module Sequel
|
|
429
430
|
@quote_identifiers = @opts.include?(:quote_identifiers) ? @opts[:quote_identifiers] : (@@quote_identifiers.nil? ? quote_identifiers_default : @@quote_identifiers)
|
430
431
|
end
|
431
432
|
|
433
|
+
# Runs the supplied SQL statement string on the database server. Returns nil.
|
434
|
+
# Options:
|
435
|
+
# * :server - The server to run the SQL on.
|
436
|
+
def run(sql, opts={})
|
437
|
+
execute_ddl(sql, opts)
|
438
|
+
nil
|
439
|
+
end
|
440
|
+
|
432
441
|
# Returns a new dataset with the select method invoked.
|
433
442
|
def select(*args, &block)
|
434
443
|
dataset.select(*args, &block)
|
@@ -924,6 +933,8 @@ module Sequel
|
|
924
933
|
Date.new(value.year, value.month, value.day)
|
925
934
|
when String
|
926
935
|
Sequel.string_to_date(value)
|
936
|
+
when Hash
|
937
|
+
Date.new(*[:year, :month, :day].map{|x| (value[x] || value[x.to_s]).to_i})
|
927
938
|
else
|
928
939
|
raise InvalidValue, "invalid value for Date: #{value.inspect}"
|
929
940
|
end
|
@@ -931,14 +942,12 @@ module Sequel
|
|
931
942
|
|
932
943
|
# Typecast the value to a DateTime or Time depending on Sequel.datetime_class
|
933
944
|
def typecast_value_datetime(value)
|
934
|
-
raise(Sequel::InvalidValue, "invalid value for Datetime: #{value.inspect}") unless [DateTime, Date, Time, String].any?{|c| value.is_a?(c)}
|
935
|
-
|
936
|
-
|
937
|
-
|
945
|
+
raise(Sequel::InvalidValue, "invalid value for Datetime: #{value.inspect}") unless [DateTime, Date, Time, String, Hash].any?{|c| value.is_a?(c)}
|
946
|
+
klass = Sequel.datetime_class
|
947
|
+
if value.is_a?(Hash)
|
948
|
+
klass.send(klass == Time ? :mktime : :new, *[:year, :month, :day, :hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
|
938
949
|
else
|
939
|
-
|
940
|
-
# parse that string using the time class.
|
941
|
-
Sequel.string_to_datetime(Time === value ? value.iso8601 : value.to_s)
|
950
|
+
Sequel.typecast_to_application_timestamp(value)
|
942
951
|
end
|
943
952
|
end
|
944
953
|
|
@@ -976,6 +985,9 @@ module Sequel
|
|
976
985
|
value
|
977
986
|
when String
|
978
987
|
Sequel.string_to_time(value)
|
988
|
+
when Hash
|
989
|
+
t = Time.now
|
990
|
+
Time.mktime(t.year, t.month, t.day, *[:hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
|
979
991
|
else
|
980
992
|
raise Sequel::InvalidValue, "invalid value for Time: #{value.inspect}"
|
981
993
|
end
|
data/lib/sequel/dataset.rb
CHANGED
@@ -42,13 +42,13 @@ module Sequel
|
|
42
42
|
|
43
43
|
# All methods that should have a ! method added that modifies
|
44
44
|
# the receiver.
|
45
|
-
MUTATION_METHODS = %w'add_graph_aliases and distinct exclude
|
45
|
+
MUTATION_METHODS = %w'add_graph_aliases and distinct except exclude
|
46
46
|
filter from from_self full_outer_join graph
|
47
|
-
group group_and_count group_by having inner_join intersect invert join
|
48
|
-
left_outer_join limit naked or order order_by order_more paginate qualify query
|
49
|
-
reverse reverse_order right_outer_join select select_all select_more
|
50
|
-
set_defaults set_graph_aliases set_overrides
|
51
|
-
|
47
|
+
group group_and_count group_by having inner_join intersect invert join join_table
|
48
|
+
left_outer_join limit naked or order order_by order_more paginate qualify query
|
49
|
+
reverse reverse_order right_outer_join select select_all select_more server
|
50
|
+
set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
|
51
|
+
unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym}
|
52
52
|
|
53
53
|
NOTIMPL_MSG = "This method must be overridden in Sequel adapters".freeze
|
54
54
|
WITH_SUPPORTED='with'.freeze
|
@@ -175,6 +175,10 @@ module Sequel
|
|
175
175
|
|
176
176
|
# Iterates over the records in the dataset as they are yielded from the
|
177
177
|
# database adapter, and returns self.
|
178
|
+
#
|
179
|
+
# Note that this method is not safe to use on many adapters if you are
|
180
|
+
# running additional queries inside the provided block. If you are
|
181
|
+
# running queries inside the block, you use should all instead of each.
|
178
182
|
def each(&block)
|
179
183
|
if @opts[:graph]
|
180
184
|
graph_each(&block)
|
@@ -275,10 +279,25 @@ module Sequel
|
|
275
279
|
true
|
276
280
|
end
|
277
281
|
|
282
|
+
# Whether the dataset supports timezones in literal timestamps
|
283
|
+
def supports_timestamp_timezones?
|
284
|
+
false
|
285
|
+
end
|
286
|
+
|
287
|
+
# Whether the dataset supports fractional seconds in literal timestamps
|
288
|
+
def supports_timestamp_usecs?
|
289
|
+
true
|
290
|
+
end
|
291
|
+
|
278
292
|
# Whether the dataset supports window functions.
|
279
293
|
def supports_window_functions?
|
280
294
|
false
|
281
295
|
end
|
296
|
+
|
297
|
+
# Truncates the dataset. Returns nil.
|
298
|
+
def truncate
|
299
|
+
execute_ddl(truncate_sql)
|
300
|
+
end
|
282
301
|
|
283
302
|
# Updates values for the dataset. The returned value is generally the
|
284
303
|
# number of rows updated, but that is adapter dependent.
|
@@ -314,6 +333,12 @@ module Sequel
|
|
314
333
|
@db.execute(sql, {:server=>@opts[:server] || :read_only}.merge(opts), &block)
|
315
334
|
end
|
316
335
|
|
336
|
+
# Execute the given SQL on the database using execute_ddl.
|
337
|
+
def execute_ddl(sql, opts={}, &block)
|
338
|
+
@db.execute_ddl(sql, default_server_opts(opts), &block)
|
339
|
+
nil
|
340
|
+
end
|
341
|
+
|
317
342
|
# Execute the given SQL on the database using execute_dui.
|
318
343
|
def execute_dui(sql, opts={}, &block)
|
319
344
|
@db.execute_dui(sql, default_server_opts(opts), &block)
|
@@ -109,7 +109,7 @@ module Sequel
|
|
109
109
|
# # this will commit every 50 records
|
110
110
|
# dataset.import([:x, :y], [[1, 2], [3, 4], ...], :slice => 50)
|
111
111
|
def import(columns, values, opts={})
|
112
|
-
return @db.transaction{execute_dui("#{insert_sql_base}#{quote_schema_table(@opts[:from].first)} (#{identifier_list(columns)})
|
112
|
+
return @db.transaction{execute_dui("#{insert_sql_base}#{quote_schema_table(@opts[:from].first)} (#{identifier_list(columns)}) #{subselect_sql(values)}")} if values.is_a?(Dataset)
|
113
113
|
|
114
114
|
return if values.empty?
|
115
115
|
raise(Error, IMPORT_ERROR_MSG) if columns.empty?
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -17,6 +17,8 @@ module Sequel
|
|
17
17
|
QUESTION_MARK = '?'.freeze
|
18
18
|
STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
|
19
19
|
SELECT_CLAUSE_ORDER = %w'with distinct columns from join where group having compounds order limit'.freeze
|
20
|
+
TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S%N%z'".freeze
|
21
|
+
STANDARD_TIMESTAMP_FORMAT = "TIMESTAMP #{TIMESTAMP_FORMAT}".freeze
|
20
22
|
TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
|
21
23
|
WILDCARD = '*'.freeze
|
22
24
|
SQL_WITH = "WITH ".freeze
|
@@ -88,6 +90,11 @@ module Sequel
|
|
88
90
|
raise(InvalidOperation, "invalid operator #{op}")
|
89
91
|
end
|
90
92
|
end
|
93
|
+
|
94
|
+
# SQL fragment for constants
|
95
|
+
def constant_sql(constant)
|
96
|
+
constant.to_s
|
97
|
+
end
|
91
98
|
|
92
99
|
# Returns the number of records in the dataset.
|
93
100
|
def count
|
@@ -103,11 +110,7 @@ module Sequel
|
|
103
110
|
|
104
111
|
return static_sql(opts[:sql]) if opts[:sql]
|
105
112
|
|
106
|
-
|
107
|
-
raise InvalidOperation, "Grouped datasets cannot be deleted from"
|
108
|
-
elsif opts[:from].is_a?(Array) && opts[:from].size > 1
|
109
|
-
raise InvalidOperation, "Joined datasets cannot be deleted from"
|
110
|
-
end
|
113
|
+
check_modification_allowed!
|
111
114
|
|
112
115
|
sql = "DELETE FROM #{source_list(opts[:from])}"
|
113
116
|
|
@@ -308,7 +311,7 @@ module Sequel
|
|
308
311
|
# dataset.group(:id) # SELECT * FROM items GROUP BY id
|
309
312
|
# dataset.group(:id, :name) # SELECT * FROM items GROUP BY id, name
|
310
313
|
def group(*columns)
|
311
|
-
clone(:group => columns)
|
314
|
+
clone(:group => (columns.compact.empty? ? nil : columns))
|
312
315
|
end
|
313
316
|
alias group_by group
|
314
317
|
|
@@ -344,6 +347,8 @@ module Sequel
|
|
344
347
|
def insert_sql(*values)
|
345
348
|
return static_sql(@opts[:sql]) if @opts[:sql]
|
346
349
|
|
350
|
+
check_modification_allowed!
|
351
|
+
|
347
352
|
from = source_list(@opts[:from])
|
348
353
|
case values.size
|
349
354
|
when 0
|
@@ -778,14 +783,32 @@ module Sequel
|
|
778
783
|
def subscript_sql(s)
|
779
784
|
"#{literal(s.f)}[#{expression_list(s.sub)}]"
|
780
785
|
end
|
786
|
+
|
787
|
+
# SQL query to truncate the table
|
788
|
+
def truncate_sql
|
789
|
+
if opts[:sql]
|
790
|
+
static_sql(opts[:sql])
|
791
|
+
else
|
792
|
+
check_modification_allowed!
|
793
|
+
raise(InvalidOperation, "Can't truncate filtered datasets") if opts[:where]
|
794
|
+
_truncate_sql(source_list(opts[:from]))
|
795
|
+
end
|
796
|
+
end
|
781
797
|
|
782
798
|
# Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
|
783
799
|
#
|
784
|
-
# dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items
|
800
|
+
# dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
|
785
801
|
def unfiltered
|
786
802
|
clone(:where => nil, :having => nil)
|
787
803
|
end
|
788
804
|
|
805
|
+
# Returns a copy of the dataset with no grouping (GROUP or HAVING clause) applied.
|
806
|
+
#
|
807
|
+
# dataset.group(:a).having(:a=>1).where(:b).ungrouped # SELECT * FROM items WHERE b
|
808
|
+
def ungrouped
|
809
|
+
clone(:group => nil, :having => nil)
|
810
|
+
end
|
811
|
+
|
789
812
|
# Adds a UNION clause using a second dataset object.
|
790
813
|
# A UNION compound dataset returns all rows in either the current dataset
|
791
814
|
# or the given dataset.
|
@@ -826,11 +849,7 @@ module Sequel
|
|
826
849
|
|
827
850
|
return static_sql(opts[:sql]) if opts[:sql]
|
828
851
|
|
829
|
-
|
830
|
-
raise InvalidOperation, "A grouped dataset cannot be updated"
|
831
|
-
elsif (opts[:from].size > 1) or opts[:join]
|
832
|
-
raise InvalidOperation, "A joined dataset cannot be updated"
|
833
|
-
end
|
852
|
+
check_modification_allowed!
|
834
853
|
|
835
854
|
sql = "UPDATE #{source_list(@opts[:from])} SET "
|
836
855
|
set = if values.is_a?(Hash)
|
@@ -945,6 +964,13 @@ module Sequel
|
|
945
964
|
def as_sql(expression, aliaz)
|
946
965
|
"#{expression} AS #{quote_identifier(aliaz)}"
|
947
966
|
end
|
967
|
+
|
968
|
+
# Raise an InvalidOperation exception if deletion is not allowed
|
969
|
+
# for this dataset
|
970
|
+
def check_modification_allowed!
|
971
|
+
raise(InvalidOperation, "Grouped datasets cannot be modified") if opts[:group]
|
972
|
+
raise(InvalidOperation, "Joined datasets cannot be modified") if (opts[:from].is_a?(Array) && opts[:from].size > 1) || opts[:join]
|
973
|
+
end
|
948
974
|
|
949
975
|
# Converts an array of column names into a comma seperated string of
|
950
976
|
# column names. If the array is empty, a wildcard (*) is returned.
|
@@ -969,6 +995,11 @@ module Sequel
|
|
969
995
|
columns.map{|i| literal(i)}.join(COMMA_SEPARATOR)
|
970
996
|
end
|
971
997
|
|
998
|
+
# The strftime format to use when literalizing the time.
|
999
|
+
def default_timestamp_format
|
1000
|
+
requires_sql_standard_datetimes? ? STANDARD_TIMESTAMP_FORMAT : TIMESTAMP_FORMAT
|
1001
|
+
end
|
1002
|
+
|
972
1003
|
# SQL fragment based on the expr type. See #filter.
|
973
1004
|
def filter_expr(expr = nil, &block)
|
974
1005
|
expr = nil if expr == []
|
@@ -1002,6 +1033,27 @@ module Sequel
|
|
1002
1033
|
raise(Error, 'Invalid filter argument')
|
1003
1034
|
end
|
1004
1035
|
end
|
1036
|
+
|
1037
|
+
# Format the timestamp based on the default_timestamp_format, with a couple
|
1038
|
+
# of modifiers. First, allow %N to be used for fractions seconds (if the
|
1039
|
+
# database supports them), and override %z to always use a numeric offset
|
1040
|
+
# of hours and minutes.
|
1041
|
+
def format_timestamp(v)
|
1042
|
+
v2 = Sequel.application_to_database_timestamp(v)
|
1043
|
+
fmt = default_timestamp_format.gsub(/%[Nz]/) do |m|
|
1044
|
+
if m == '%N'
|
1045
|
+
sprintf(".%06d", v.is_a?(DateTime) ? v.sec_fraction*86400000000 : v.usec) if supports_timestamp_usecs?
|
1046
|
+
else
|
1047
|
+
if supports_timestamp_timezones?
|
1048
|
+
# Would like to just use %z format, but it doesn't appear to work on Windows
|
1049
|
+
# Instead, the offset fragment is constructed manually
|
1050
|
+
minutes = (v2.is_a?(DateTime) ? v2.offset * 1440 : v2.utc_offset/60).to_i
|
1051
|
+
sprintf("%+03i%02i", *minutes.divmod(60))
|
1052
|
+
end
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
v2.strftime(fmt)
|
1056
|
+
end
|
1005
1057
|
|
1006
1058
|
# SQL fragment specifying a list of identifiers
|
1007
1059
|
def identifier_list(columns)
|
@@ -1035,7 +1087,7 @@ module Sequel
|
|
1035
1087
|
order.map do |f|
|
1036
1088
|
case f
|
1037
1089
|
when SQL::OrderedExpression
|
1038
|
-
|
1090
|
+
f.invert
|
1039
1091
|
else
|
1040
1092
|
SQL::OrderedExpression.new(f)
|
1041
1093
|
end
|
@@ -1074,9 +1126,9 @@ module Sequel
|
|
1074
1126
|
requires_sql_standard_datetimes? ? v.strftime("DATE '%Y-%m-%d'") : "'#{v}'"
|
1075
1127
|
end
|
1076
1128
|
|
1077
|
-
# SQL fragment for DateTime
|
1129
|
+
# SQL fragment for DateTime
|
1078
1130
|
def literal_datetime(v)
|
1079
|
-
|
1131
|
+
format_timestamp(v)
|
1080
1132
|
end
|
1081
1133
|
|
1082
1134
|
# SQL fragment for SQL::Expression, result depends on the specific type of expression.
|
@@ -1128,12 +1180,12 @@ module Sequel
|
|
1128
1180
|
c_alias ? as_sql(qc, c_alias) : qc
|
1129
1181
|
end
|
1130
1182
|
|
1131
|
-
# SQL fragment for Time
|
1183
|
+
# SQL fragment for Time
|
1132
1184
|
def literal_time(v)
|
1133
|
-
|
1185
|
+
format_timestamp(v)
|
1134
1186
|
end
|
1135
1187
|
|
1136
|
-
# SQL fragment for true
|
1188
|
+
# SQL fragment for true
|
1137
1189
|
def literal_true
|
1138
1190
|
BOOL_TRUE
|
1139
1191
|
end
|
@@ -1323,5 +1375,11 @@ module Sequel
|
|
1323
1375
|
def table_ref(t)
|
1324
1376
|
t.is_a?(String) ? quote_identifier(t) : literal(t)
|
1325
1377
|
end
|
1378
|
+
|
1379
|
+
# Formats the truncate statement. Assumes the table given has already been
|
1380
|
+
# literalized.
|
1381
|
+
def _truncate_sql(table)
|
1382
|
+
"TRUNCATE TABLE #{table}"
|
1383
|
+
end
|
1326
1384
|
end
|
1327
1385
|
end
|