sequel 3.5.0 → 3.6.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 +108 -0
- data/README.rdoc +25 -14
- data/Rakefile +20 -1
- data/doc/advanced_associations.rdoc +61 -64
- data/doc/cheat_sheet.rdoc +16 -7
- data/doc/opening_databases.rdoc +3 -3
- data/doc/prepared_statements.rdoc +1 -1
- data/doc/reflection.rdoc +2 -1
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/schema.rdoc +19 -14
- data/lib/sequel/adapters/amalgalite.rb +5 -27
- data/lib/sequel/adapters/jdbc.rb +13 -3
- data/lib/sequel/adapters/jdbc/h2.rb +17 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
- data/lib/sequel/adapters/mysql.rb +4 -3
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +87 -28
- data/lib/sequel/adapters/shared/mssql.rb +47 -6
- data/lib/sequel/adapters/shared/mysql.rb +12 -31
- data/lib/sequel/adapters/shared/postgres.rb +15 -12
- data/lib/sequel/adapters/shared/sqlite.rb +18 -0
- data/lib/sequel/adapters/sqlite.rb +1 -16
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +2 -0
- data/lib/sequel/database/schema_sql.rb +1 -1
- data/lib/sequel/dataset.rb +5 -179
- data/lib/sequel/dataset/actions.rb +123 -0
- data/lib/sequel/dataset/convenience.rb +18 -10
- data/lib/sequel/dataset/features.rb +65 -0
- data/lib/sequel/dataset/prepared_statements.rb +29 -23
- data/lib/sequel/dataset/query.rb +429 -0
- data/lib/sequel/dataset/sql.rb +67 -435
- data/lib/sequel/model/associations.rb +77 -13
- data/lib/sequel/model/base.rb +30 -8
- data/lib/sequel/model/errors.rb +4 -4
- data/lib/sequel/plugins/caching.rb +38 -15
- data/lib/sequel/plugins/force_encoding.rb +18 -7
- data/lib/sequel/plugins/hook_class_methods.rb +4 -0
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +40 -11
- data/lib/sequel/plugins/serialization.rb +17 -3
- data/lib/sequel/plugins/validation_helpers.rb +65 -18
- data/lib/sequel/sql.rb +23 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +96 -10
- data/spec/adapters/mysql_spec.rb +19 -0
- data/spec/adapters/postgres_spec.rb +65 -2
- data/spec/adapters/sqlite_spec.rb +10 -0
- data/spec/core/core_sql_spec.rb +9 -0
- data/spec/core/database_spec.rb +8 -4
- data/spec/core/dataset_spec.rb +122 -29
- data/spec/core/expression_filters_spec.rb +17 -0
- data/spec/extensions/caching_spec.rb +43 -3
- data/spec/extensions/force_encoding_spec.rb +43 -1
- data/spec/extensions/nested_attributes_spec.rb +55 -2
- data/spec/extensions/validation_helpers_spec.rb +71 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/dataset_test.rb +383 -9
- data/spec/integration/eager_loader_test.rb +0 -65
- data/spec/integration/model_test.rb +110 -0
- data/spec/integration/plugin_test.rb +306 -0
- data/spec/integration/prepared_statement_test.rb +32 -0
- data/spec/integration/schema_test.rb +8 -3
- data/spec/integration/spec_helper.rb +1 -25
- data/spec/model/association_reflection_spec.rb +38 -0
- data/spec/model/associations_spec.rb +184 -8
- data/spec/model/eager_loading_spec.rb +23 -0
- data/spec/model/model_spec.rb +8 -0
- data/spec/model/record_spec.rb +84 -1
- metadata +9 -2
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -7,6 +7,17 @@ module Sequel
|
|
7
7
|
clauses.map{|clause| :"#{type}_#{clause}_sql"}.freeze
|
8
8
|
end
|
9
9
|
|
10
|
+
# These symbols have _join methods created (e.g. inner_join) that
|
11
|
+
# call join_table with the symbol, passing along the arguments and
|
12
|
+
# block from the method call.
|
13
|
+
CONDITIONED_JOIN_TYPES = [:inner, :full_outer, :right_outer, :left_outer, :full, :right, :left]
|
14
|
+
|
15
|
+
# These symbols have _join methods created (e.g. natural_join) that
|
16
|
+
# call join_table with the symbol. They only accept a single table
|
17
|
+
# argument which is passed to join_table, and they raise an error
|
18
|
+
# if called with a block.
|
19
|
+
UNCONDITIONED_JOIN_TYPES = [:natural, :natural_left, :natural_right, :natural_full, :cross]
|
20
|
+
|
10
21
|
AND_SEPARATOR = " AND ".freeze
|
11
22
|
BOOL_FALSE = "'f'".freeze
|
12
23
|
BOOL_TRUE = "'t'".freeze
|
@@ -15,14 +26,12 @@ module Sequel
|
|
15
26
|
COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
|
16
27
|
COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
|
17
28
|
DATASET_ALIAS_BASE_NAME = 't'.freeze
|
18
|
-
FROM_SELF_KEEP_OPTS = [:graph, :eager_graph, :graph_aliases]
|
19
29
|
IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
|
20
30
|
IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
|
21
31
|
N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
|
22
32
|
NULL = "NULL".freeze
|
23
33
|
QUALIFY_KEYS = [:select, :where, :having, :order, :group]
|
24
34
|
QUESTION_MARK = '?'.freeze
|
25
|
-
STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
|
26
35
|
DELETE_CLAUSE_METHODS = clause_methods(:delete, %w'from where')
|
27
36
|
INSERT_CLAUSE_METHODS = clause_methods(:insert, %w'into columns values')
|
28
37
|
SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct columns from join where group having compounds order limit')
|
@@ -33,16 +42,6 @@ module Sequel
|
|
33
42
|
WILDCARD = '*'.freeze
|
34
43
|
SQL_WITH = "WITH ".freeze
|
35
44
|
|
36
|
-
# Adds an further filter to an existing filter using AND. If no filter
|
37
|
-
# exists an error is raised. This method is identical to #filter except
|
38
|
-
# it expects an existing filter.
|
39
|
-
#
|
40
|
-
# ds.filter(:a).and(:b) # SQL: WHERE a AND b
|
41
|
-
def and(*cond, &block)
|
42
|
-
raise(InvalidOperation, "No existing filter found.") unless @opts[:having] || @opts[:where]
|
43
|
-
filter(*cond, &block)
|
44
|
-
end
|
45
|
-
|
46
45
|
# SQL fragment for the aliased expression
|
47
46
|
def aliased_expression_sql(ae)
|
48
47
|
as_sql(literal(ae.expression), ae.aliaz)
|
@@ -86,6 +85,14 @@ module Sequel
|
|
86
85
|
else
|
87
86
|
complex_expression_sql(:OR, [SQL::BooleanExpression.new(:"!=", *args), SQL::BooleanExpression.new(:IS, args.at(0), nil)])
|
88
87
|
end
|
88
|
+
when :IN, :"NOT IN"
|
89
|
+
cols = args.at(0)
|
90
|
+
if !supports_multiple_column_in? && cols.is_a?(Array)
|
91
|
+
expr = SQL::BooleanExpression.new(:OR, *args.at(1).to_a.map{|vals| SQL::BooleanExpression.from_value_pairs(cols.zip(vals).map{|col, val| [col, val]})})
|
92
|
+
literal(op == :IN ? expr : ~expr)
|
93
|
+
else
|
94
|
+
"(#{literal(cols)} #{op} #{literal(args.at(1))})"
|
95
|
+
end
|
89
96
|
when *TWO_ARITY_OPERATORS
|
90
97
|
"(#{literal(args.at(0))} #{op} #{literal(args.at(1))})"
|
91
98
|
when *N_ARITY_OPERATORS
|
@@ -108,7 +115,7 @@ module Sequel
|
|
108
115
|
|
109
116
|
# Returns the number of records in the dataset.
|
110
117
|
def count
|
111
|
-
|
118
|
+
aggregate_dataset.get{COUNT(:*){}.as(count)}.to_i
|
112
119
|
end
|
113
120
|
|
114
121
|
# Formats a DELETE statement using the given options and dataset options.
|
@@ -121,50 +128,6 @@ module Sequel
|
|
121
128
|
clause_sql(:delete)
|
122
129
|
end
|
123
130
|
|
124
|
-
# Returns a copy of the dataset with the SQL DISTINCT clause.
|
125
|
-
# The DISTINCT clause is used to remove duplicate rows from the
|
126
|
-
# output. If arguments are provided, uses a DISTINCT ON clause,
|
127
|
-
# in which case it will only be distinct on those columns, instead
|
128
|
-
# of all returned columns. Raises an error if arguments
|
129
|
-
# are given and DISTINCT ON is not supported.
|
130
|
-
#
|
131
|
-
# dataset.distinct # SQL: SELECT DISTINCT * FROM items
|
132
|
-
# dataset.order(:id).distinct(:id) # SQL: SELECT DISTINCT ON (id) * FROM items ORDER BY id
|
133
|
-
def distinct(*args)
|
134
|
-
raise(InvalidOperation, "DISTINCT ON not supported") if !args.empty? && !supports_distinct_on?
|
135
|
-
clone(:distinct => args)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Adds an EXCEPT clause using a second dataset object.
|
139
|
-
# An EXCEPT compound dataset returns all rows in the current dataset
|
140
|
-
# that are not in the given dataset.
|
141
|
-
# Raises an InvalidOperation if the operation is not supported.
|
142
|
-
# Options:
|
143
|
-
# * :all - Set to true to use EXCEPT ALL instead of EXCEPT, so duplicate rows can occur
|
144
|
-
# * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
|
145
|
-
#
|
146
|
-
# DB[:items].except(DB[:other_items]).sql
|
147
|
-
# #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
|
148
|
-
def except(dataset, opts={})
|
149
|
-
opts = {:all=>opts} unless opts.is_a?(Hash)
|
150
|
-
raise(InvalidOperation, "EXCEPT not supported") unless supports_intersect_except?
|
151
|
-
raise(InvalidOperation, "EXCEPT ALL not supported") if opts[:all] && !supports_intersect_except_all?
|
152
|
-
compound_clone(:except, dataset, opts)
|
153
|
-
end
|
154
|
-
|
155
|
-
# Performs the inverse of Dataset#filter.
|
156
|
-
#
|
157
|
-
# dataset.exclude(:category => 'software').sql #=>
|
158
|
-
# "SELECT * FROM items WHERE (category != 'software')"
|
159
|
-
def exclude(*cond, &block)
|
160
|
-
clause = (@opts[:having] ? :having : :where)
|
161
|
-
cond = cond.first if cond.size == 1
|
162
|
-
cond = filter_expr(cond, &block)
|
163
|
-
cond = SQL::BooleanExpression.invert(cond)
|
164
|
-
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
165
|
-
clone(clause => cond)
|
166
|
-
end
|
167
|
-
|
168
131
|
# Returns an EXISTS clause for the dataset as a LiteralString.
|
169
132
|
#
|
170
133
|
# DB.select(1).where(DB[:items].exists).sql
|
@@ -173,57 +136,6 @@ module Sequel
|
|
173
136
|
LiteralString.new("EXISTS (#{select_sql})")
|
174
137
|
end
|
175
138
|
|
176
|
-
# Returns a copy of the dataset with the given conditions imposed upon it.
|
177
|
-
# If the query already has a HAVING clause, then the conditions are imposed in the
|
178
|
-
# HAVING clause. If not, then they are imposed in the WHERE clause.
|
179
|
-
#
|
180
|
-
# filter accepts the following argument types:
|
181
|
-
#
|
182
|
-
# * Hash - list of equality/inclusion expressions
|
183
|
-
# * Array - depends:
|
184
|
-
# * If first member is a string, assumes the rest of the arguments
|
185
|
-
# are parameters and interpolates them into the string.
|
186
|
-
# * If all members are arrays of length two, treats the same way
|
187
|
-
# as a hash, except it allows for duplicate keys to be
|
188
|
-
# specified.
|
189
|
-
# * String - taken literally
|
190
|
-
# * Symbol - taken as a boolean column argument (e.g. WHERE active)
|
191
|
-
# * Sequel::SQL::BooleanExpression - an existing condition expression,
|
192
|
-
# probably created using the Sequel expression filter DSL.
|
193
|
-
#
|
194
|
-
# filter also takes a block, which should return one of the above argument
|
195
|
-
# types, and is treated the same way. This block yields a virtual row object,
|
196
|
-
# which is easy to use to create identifiers and functions.
|
197
|
-
#
|
198
|
-
# If both a block and regular argument
|
199
|
-
# are provided, they get ANDed together.
|
200
|
-
#
|
201
|
-
# Examples:
|
202
|
-
#
|
203
|
-
# dataset.filter(:id => 3).sql #=>
|
204
|
-
# "SELECT * FROM items WHERE (id = 3)"
|
205
|
-
# dataset.filter('price < ?', 100).sql #=>
|
206
|
-
# "SELECT * FROM items WHERE price < 100"
|
207
|
-
# dataset.filter([[:id, (1,2,3)], [:id, 0..10]]).sql #=>
|
208
|
-
# "SELECT * FROM items WHERE ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10)))"
|
209
|
-
# dataset.filter('price < 100').sql #=>
|
210
|
-
# "SELECT * FROM items WHERE price < 100"
|
211
|
-
# dataset.filter(:active).sql #=>
|
212
|
-
# "SELECT * FROM items WHERE :active
|
213
|
-
# dataset.filter{|o| o.price < 100}.sql #=>
|
214
|
-
# "SELECT * FROM items WHERE (price < 100)"
|
215
|
-
#
|
216
|
-
# Multiple filter calls can be chained for scoping:
|
217
|
-
#
|
218
|
-
# software = dataset.filter(:category => 'software')
|
219
|
-
# software.filter{|o| o.price < 100}.sql #=>
|
220
|
-
# "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
|
221
|
-
#
|
222
|
-
# See doc/dataset_filters.rdoc for more examples and details.
|
223
|
-
def filter(*cond, &block)
|
224
|
-
_filter(@opts[:having] ? :having : :where, *cond, &block)
|
225
|
-
end
|
226
|
-
|
227
139
|
# The first source (primary table) for this dataset. If the dataset doesn't
|
228
140
|
# have a table, raises an error. If the table is aliased, returns the aliased name.
|
229
141
|
def first_source_alias
|
@@ -243,87 +155,12 @@ module Sequel
|
|
243
155
|
end
|
244
156
|
alias first_source first_source_alias
|
245
157
|
|
246
|
-
# Returns a copy of the dataset with the source changed.
|
247
|
-
#
|
248
|
-
# dataset.from # SQL: SELECT *
|
249
|
-
# dataset.from(:blah) # SQL: SELECT * FROM blah
|
250
|
-
# dataset.from(:blah, :foo) # SQL: SELECT * FROM blah, foo
|
251
|
-
def from(*source)
|
252
|
-
table_alias_num = 0
|
253
|
-
sources = []
|
254
|
-
source.each do |s|
|
255
|
-
case s
|
256
|
-
when Hash
|
257
|
-
s.each{|k,v| sources << SQL::AliasedExpression.new(k,v)}
|
258
|
-
when Dataset
|
259
|
-
sources << SQL::AliasedExpression.new(s, dataset_alias(table_alias_num+=1))
|
260
|
-
when Symbol
|
261
|
-
sch, table, aliaz = split_symbol(s)
|
262
|
-
if aliaz
|
263
|
-
s = sch ? SQL::QualifiedIdentifier.new(sch.to_sym, table.to_sym) : SQL::Identifier.new(table.to_sym)
|
264
|
-
sources << SQL::AliasedExpression.new(s, aliaz.to_sym)
|
265
|
-
else
|
266
|
-
sources << s
|
267
|
-
end
|
268
|
-
else
|
269
|
-
sources << s
|
270
|
-
end
|
271
|
-
end
|
272
|
-
o = {:from=>sources.empty? ? nil : sources}
|
273
|
-
o[:num_dataset_sources] = table_alias_num if table_alias_num > 0
|
274
|
-
clone(o)
|
275
|
-
end
|
276
|
-
|
277
|
-
# Returns a dataset selecting from the current dataset.
|
278
|
-
# Supplying the :alias option controls the name of the result.
|
279
|
-
#
|
280
|
-
# ds = DB[:items].order(:name).select(:id, :name)
|
281
|
-
# ds.sql #=> "SELECT id,name FROM items ORDER BY name"
|
282
|
-
# ds.from_self.sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 't1'"
|
283
|
-
# ds.from_self(:alias=>:foo).sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 'foo'"
|
284
|
-
def from_self(opts={})
|
285
|
-
fs = {}
|
286
|
-
@opts.keys.each{|k| fs[k] = nil unless FROM_SELF_KEEP_OPTS.include?(k)}
|
287
|
-
clone(fs).from(opts[:alias] ? as(opts[:alias]) : self)
|
288
|
-
end
|
289
|
-
|
290
158
|
# SQL fragment specifying an SQL function call
|
291
159
|
def function_sql(f)
|
292
160
|
args = f.args
|
293
161
|
"#{f.f}#{args.empty? ? '()' : literal(args)}"
|
294
162
|
end
|
295
163
|
|
296
|
-
# Pattern match any of the columns to any of the terms. The terms can be
|
297
|
-
# strings (which use LIKE) or regular expressions (which are only supported
|
298
|
-
# in some databases). See Sequel::SQL::StringExpression.like. Note that the
|
299
|
-
# total number of pattern matches will be cols.length * terms.length,
|
300
|
-
# which could cause performance issues.
|
301
|
-
#
|
302
|
-
# dataset.grep(:a, '%test%') # SQL: SELECT * FROM items WHERE a LIKE '%test%'
|
303
|
-
# dataset.grep([:a, :b], %w'%test% foo') # SQL: SELECT * FROM items WHERE a LIKE '%test%' OR a LIKE 'foo' OR b LIKE '%test%' OR b LIKE 'foo'
|
304
|
-
def grep(cols, terms)
|
305
|
-
filter(SQL::BooleanExpression.new(:OR, *Array(cols).collect{|c| SQL::StringExpression.like(c, *terms)}))
|
306
|
-
end
|
307
|
-
|
308
|
-
# Returns a copy of the dataset with the results grouped by the value of
|
309
|
-
# the given columns.
|
310
|
-
#
|
311
|
-
# dataset.group(:id) # SELECT * FROM items GROUP BY id
|
312
|
-
# dataset.group(:id, :name) # SELECT * FROM items GROUP BY id, name
|
313
|
-
def group(*columns)
|
314
|
-
clone(:group => (columns.compact.empty? ? nil : columns))
|
315
|
-
end
|
316
|
-
alias group_by group
|
317
|
-
|
318
|
-
# Returns a copy of the dataset with the HAVING conditions changed. Raises
|
319
|
-
# an error if the dataset has not been grouped. See #filter for argument types.
|
320
|
-
#
|
321
|
-
# dataset.group(:sum).having(:sum=>10) # SQL: SELECT * FROM items GROUP BY sum HAVING sum = 10
|
322
|
-
def having(*cond, &block)
|
323
|
-
raise(InvalidOperation, "Can only specify a HAVING clause on a grouped dataset") unless @opts[:group]
|
324
|
-
_filter(:having, *cond, &block)
|
325
|
-
end
|
326
|
-
|
327
164
|
# Inserts multiple values. If a block is given it is invoked for each
|
328
165
|
# item in the given array before inserting it. See #multi_insert as
|
329
166
|
# a possible faster version that inserts multiple records in one
|
@@ -390,36 +227,6 @@ module Sequel
|
|
390
227
|
clone(:columns=>columns, :values=>values)._insert_sql
|
391
228
|
end
|
392
229
|
|
393
|
-
# Adds an INTERSECT clause using a second dataset object.
|
394
|
-
# An INTERSECT compound dataset returns all rows in both the current dataset
|
395
|
-
# and the given dataset.
|
396
|
-
# Raises an InvalidOperation if the operation is not supported.
|
397
|
-
# Options:
|
398
|
-
# * :all - Set to true to use INTERSECT ALL instead of INTERSECT, so duplicate rows can occur
|
399
|
-
# * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
|
400
|
-
#
|
401
|
-
# DB[:items].intersect(DB[:other_items]).sql
|
402
|
-
# #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
|
403
|
-
def intersect(dataset, opts={})
|
404
|
-
opts = {:all=>opts} unless opts.is_a?(Hash)
|
405
|
-
raise(InvalidOperation, "INTERSECT not supported") unless supports_intersect_except?
|
406
|
-
raise(InvalidOperation, "INTERSECT ALL not supported") if opts[:all] && !supports_intersect_except_all?
|
407
|
-
compound_clone(:intersect, dataset, opts)
|
408
|
-
end
|
409
|
-
|
410
|
-
# Inverts the current filter
|
411
|
-
#
|
412
|
-
# dataset.filter(:category => 'software').invert.sql #=>
|
413
|
-
# "SELECT * FROM items WHERE (category != 'software')"
|
414
|
-
def invert
|
415
|
-
having, where = @opts[:having], @opts[:where]
|
416
|
-
raise(Error, "No current filter") unless having || where
|
417
|
-
o = {}
|
418
|
-
o[:having] = SQL::BooleanExpression.invert(having) if having
|
419
|
-
o[:where] = SQL::BooleanExpression.invert(where) if where
|
420
|
-
clone(o)
|
421
|
-
end
|
422
|
-
|
423
230
|
# SQL fragment specifying a JOIN clause without ON or USING.
|
424
231
|
def join_clause_sql(jc)
|
425
232
|
table = jc.table
|
@@ -470,6 +277,13 @@ module Sequel
|
|
470
277
|
# the table alias/name for the last joined (or first table), and an array of previous
|
471
278
|
# SQL::JoinClause.
|
472
279
|
def join_table(type, table, expr=nil, options={}, &block)
|
280
|
+
using_join = expr.is_a?(Array) && !expr.empty? && expr.all?{|x| x.is_a?(Symbol)}
|
281
|
+
if using_join && !supports_join_using?
|
282
|
+
h = {}
|
283
|
+
expr.each{|s| h[s] = s}
|
284
|
+
return join_table(type, table, h, options)
|
285
|
+
end
|
286
|
+
|
473
287
|
if [Symbol, String].any?{|c| options.is_a?(c)}
|
474
288
|
table_alias = options
|
475
289
|
last_alias = nil
|
@@ -490,7 +304,7 @@ module Sequel
|
|
490
304
|
|
491
305
|
join = if expr.nil? and !block_given?
|
492
306
|
SQL::JoinClause.new(type, table, table_alias)
|
493
|
-
elsif
|
307
|
+
elsif using_join
|
494
308
|
raise(Sequel::Error, "can't use a block if providing an array of symbols as expr") if block_given?
|
495
309
|
SQL::JoinUsingClause.new(expr, type, table, table_alias)
|
496
310
|
else
|
@@ -514,30 +328,6 @@ module Sequel
|
|
514
328
|
clone(opts)
|
515
329
|
end
|
516
330
|
|
517
|
-
# If given an integer, the dataset will contain only the first l results.
|
518
|
-
# If given a range, it will contain only those at offsets within that
|
519
|
-
# range. If a second argument is given, it is used as an offset.
|
520
|
-
#
|
521
|
-
# dataset.limit(10) # SQL: SELECT * FROM items LIMIT 10
|
522
|
-
# dataset.limit(10, 20) # SQL: SELECT * FROM items LIMIT 10 OFFSET 20
|
523
|
-
def limit(l, o = nil)
|
524
|
-
return from_self.limit(l, o) if @opts[:sql]
|
525
|
-
|
526
|
-
if Range === l
|
527
|
-
o = l.first
|
528
|
-
l = l.last - l.first + (l.exclude_end? ? 0 : 1)
|
529
|
-
end
|
530
|
-
l = l.to_i
|
531
|
-
raise(Error, 'Limits must be greater than or equal to 1') unless l >= 1
|
532
|
-
opts = {:limit => l}
|
533
|
-
if o
|
534
|
-
o = o.to_i
|
535
|
-
raise(Error, 'Offsets must be greater than or equal to 0') unless o >= 0
|
536
|
-
opts[:offset] = o
|
537
|
-
end
|
538
|
-
clone(opts)
|
539
|
-
end
|
540
|
-
|
541
331
|
# Returns a literal representation of a value to be used as part
|
542
332
|
# of an SQL expression.
|
543
333
|
#
|
@@ -596,46 +386,6 @@ module Sequel
|
|
596
386
|
values.map{|r| insert_sql(columns, r)}
|
597
387
|
end
|
598
388
|
|
599
|
-
# Adds an alternate filter to an existing filter using OR. If no filter
|
600
|
-
# exists an error is raised.
|
601
|
-
#
|
602
|
-
# dataset.filter(:a).or(:b) # SQL: SELECT * FROM items WHERE a OR b
|
603
|
-
def or(*cond, &block)
|
604
|
-
clause = (@opts[:having] ? :having : :where)
|
605
|
-
raise(InvalidOperation, "No existing filter found.") unless @opts[clause]
|
606
|
-
cond = cond.first if cond.size == 1
|
607
|
-
clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(cond, &block)))
|
608
|
-
end
|
609
|
-
|
610
|
-
# Returns a copy of the dataset with the order changed. If a nil is given
|
611
|
-
# the returned dataset has no order. This can accept multiple arguments
|
612
|
-
# of varying kinds, and even SQL functions. If a block is given, it is treated
|
613
|
-
# as a virtual row block, similar to filter.
|
614
|
-
#
|
615
|
-
# ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
|
616
|
-
# ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
|
617
|
-
# ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
|
618
|
-
# ds.order(:a + :b).sql #=> 'SELECT * FROM items ORDER BY (a + b)'
|
619
|
-
# ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
|
620
|
-
# ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
|
621
|
-
# ds.order{|o| o.sum(:name)}.sql #=> 'SELECT * FROM items ORDER BY sum(name)'
|
622
|
-
# ds.order(nil).sql #=> 'SELECT * FROM items'
|
623
|
-
def order(*columns, &block)
|
624
|
-
columns += Array(Sequel.virtual_row(&block)) if block
|
625
|
-
clone(:order => (columns.compact.empty?) ? nil : columns)
|
626
|
-
end
|
627
|
-
alias_method :order_by, :order
|
628
|
-
|
629
|
-
# Returns a copy of the dataset with the order columns added
|
630
|
-
# to the existing order.
|
631
|
-
#
|
632
|
-
# ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
|
633
|
-
# ds.order(:a).order_more(:b).sql #=> 'SELECT * FROM items ORDER BY a, b'
|
634
|
-
def order_more(*columns, &block)
|
635
|
-
columns = @opts[:order] + columns if @opts[:order]
|
636
|
-
order(*columns, &block)
|
637
|
-
end
|
638
|
-
|
639
389
|
# SQL fragment for the ordered expression, used in the ORDER BY
|
640
390
|
# clause.
|
641
391
|
def ordered_expression_sql(oe)
|
@@ -644,8 +394,14 @@ module Sequel
|
|
644
394
|
|
645
395
|
# SQL fragment for a literal string with placeholders
|
646
396
|
def placeholder_literal_string_sql(pls)
|
647
|
-
args = pls.args
|
648
|
-
s =
|
397
|
+
args = pls.args
|
398
|
+
s = if args.is_a?(Hash)
|
399
|
+
re = /:(#{args.keys.map{|k| Regexp.escape(k.to_s)}.join('|')})\b/
|
400
|
+
pls.str.gsub(re){literal(args[$1.to_sym])}
|
401
|
+
else
|
402
|
+
i = -1
|
403
|
+
pls.str.gsub(QUESTION_MARK){literal(args.at(i+=1))}
|
404
|
+
end
|
649
405
|
s = "(#{s})" if pls.parens
|
650
406
|
s
|
651
407
|
end
|
@@ -710,13 +466,6 @@ module Sequel
|
|
710
466
|
"\"#{name.to_s.gsub('"', '""')}\""
|
711
467
|
end
|
712
468
|
|
713
|
-
# Returns a copy of the dataset with the order reversed. If no order is
|
714
|
-
# given, the existing order is inverted.
|
715
|
-
def reverse_order(*order)
|
716
|
-
order(*invert_order(order.empty? ? @opts[:order] : order))
|
717
|
-
end
|
718
|
-
alias reverse reverse_order
|
719
|
-
|
720
469
|
# Split the schema information from the table
|
721
470
|
def schema_and_table(table_name)
|
722
471
|
sch = db.default_schema if db
|
@@ -735,39 +484,6 @@ module Sequel
|
|
735
484
|
end
|
736
485
|
end
|
737
486
|
|
738
|
-
# Returns a copy of the dataset with the columns selected changed
|
739
|
-
# to the given columns. This also takes a virtual row block,
|
740
|
-
# similar to filter.
|
741
|
-
#
|
742
|
-
# dataset.select(:a) # SELECT a FROM items
|
743
|
-
# dataset.select(:a, :b) # SELECT a, b FROM items
|
744
|
-
# dataset.select{|o| o.a, o.sum(:b)} # SELECT a, sum(b) FROM items
|
745
|
-
def select(*columns, &block)
|
746
|
-
columns += Array(Sequel.virtual_row(&block)) if block
|
747
|
-
m = []
|
748
|
-
columns.map do |i|
|
749
|
-
i.is_a?(Hash) ? m.concat(i.map{|k, v| SQL::AliasedExpression.new(k,v)}) : m << i
|
750
|
-
end
|
751
|
-
clone(:select => m)
|
752
|
-
end
|
753
|
-
|
754
|
-
# Returns a copy of the dataset selecting the wildcard.
|
755
|
-
#
|
756
|
-
# dataset.select(:a).select_all # SELECT * FROM items
|
757
|
-
def select_all
|
758
|
-
clone(:select => nil)
|
759
|
-
end
|
760
|
-
|
761
|
-
# Returns a copy of the dataset with the given columns added
|
762
|
-
# to the existing selected columns.
|
763
|
-
#
|
764
|
-
# dataset.select(:a).select(:b) # SELECT b FROM items
|
765
|
-
# dataset.select(:a).select_more(:b) # SELECT a, b FROM items
|
766
|
-
def select_more(*columns, &block)
|
767
|
-
columns = @opts[:select] + columns if @opts[:select]
|
768
|
-
select(*columns, &block)
|
769
|
-
end
|
770
|
-
|
771
487
|
# Formats a SELECT statement
|
772
488
|
#
|
773
489
|
# dataset.select_sql # => "SELECT * FROM items"
|
@@ -797,48 +513,6 @@ module Sequel
|
|
797
513
|
end
|
798
514
|
end
|
799
515
|
|
800
|
-
# Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
|
801
|
-
#
|
802
|
-
# dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
|
803
|
-
def unfiltered
|
804
|
-
clone(:where => nil, :having => nil)
|
805
|
-
end
|
806
|
-
|
807
|
-
# Returns a copy of the dataset with no grouping (GROUP or HAVING clause) applied.
|
808
|
-
#
|
809
|
-
# dataset.group(:a).having(:a=>1).where(:b).ungrouped # SELECT * FROM items WHERE b
|
810
|
-
def ungrouped
|
811
|
-
clone(:group => nil, :having => nil)
|
812
|
-
end
|
813
|
-
|
814
|
-
# Adds a UNION clause using a second dataset object.
|
815
|
-
# A UNION compound dataset returns all rows in either the current dataset
|
816
|
-
# or the given dataset.
|
817
|
-
# Options:
|
818
|
-
# * :all - Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
|
819
|
-
# * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
|
820
|
-
#
|
821
|
-
# DB[:items].union(DB[:other_items]).sql
|
822
|
-
# #=> "SELECT * FROM items UNION SELECT * FROM other_items"
|
823
|
-
def union(dataset, opts={})
|
824
|
-
opts = {:all=>opts} unless opts.is_a?(Hash)
|
825
|
-
compound_clone(:union, dataset, opts)
|
826
|
-
end
|
827
|
-
|
828
|
-
# Returns a copy of the dataset with no limit or offset.
|
829
|
-
#
|
830
|
-
# dataset.limit(10, 20).unlimited # SELECT * FROM items
|
831
|
-
def unlimited
|
832
|
-
clone(:limit=>nil, :offset=>nil)
|
833
|
-
end
|
834
|
-
|
835
|
-
# Returns a copy of the dataset with no order.
|
836
|
-
#
|
837
|
-
# dataset.order(:a).unordered # SELECT * FROM items
|
838
|
-
def unordered
|
839
|
-
order(nil)
|
840
|
-
end
|
841
|
-
|
842
516
|
# Formats an UPDATE statement using the given values.
|
843
517
|
#
|
844
518
|
# dataset.update_sql(:price => 100, :category => 'software') #=>
|
@@ -913,9 +587,12 @@ module Sequel
|
|
913
587
|
clone(:sql=>sql)
|
914
588
|
end
|
915
589
|
|
916
|
-
|
590
|
+
CONDITIONED_JOIN_TYPES.each do |jtype|
|
917
591
|
class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end", __FILE__, __LINE__)
|
918
592
|
end
|
593
|
+
UNCONDITIONED_JOIN_TYPES.each do |jtype|
|
594
|
+
class_eval("def #{jtype}_join(table); raise(Sequel::Error, '#{jtype}_join does not accept join table blocks') if block_given?; join_table(:#{jtype}, table) end", __FILE__, __LINE__)
|
595
|
+
end
|
919
596
|
alias join inner_join
|
920
597
|
|
921
598
|
protected
|
@@ -938,20 +615,20 @@ module Sequel
|
|
938
615
|
|
939
616
|
private
|
940
617
|
|
941
|
-
# Internal filter method so it works on either the having or where clauses.
|
942
|
-
def _filter(clause, *cond, &block)
|
943
|
-
cond = cond.first if cond.size == 1
|
944
|
-
cond = filter_expr(cond, &block)
|
945
|
-
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
946
|
-
clone(clause => cond)
|
947
|
-
end
|
948
|
-
|
949
618
|
# Formats the truncate statement. Assumes the table given has already been
|
950
619
|
# literalized.
|
951
620
|
def _truncate_sql(table)
|
952
621
|
"TRUNCATE TABLE #{table}"
|
953
622
|
end
|
954
623
|
|
624
|
+
# Clone of this dataset usable in aggregate operations. Does
|
625
|
+
# a from_self if dataset contains any parameters that would
|
626
|
+
# affect normal aggregation, or just removes an existing
|
627
|
+
# order if not.
|
628
|
+
def aggregate_dataset
|
629
|
+
options_overlap(COUNT_FROM_SELF_OPTS) ? from_self : unordered
|
630
|
+
end
|
631
|
+
|
955
632
|
# Do a simple join of the arguments (which should be strings or symbols) separated by commas
|
956
633
|
def argument_list(args)
|
957
634
|
args.join(COMMA_SEPARATOR)
|
@@ -982,12 +659,6 @@ module Sequel
|
|
982
659
|
(columns.nil? || columns.empty?) ? WILDCARD : expression_list(columns)
|
983
660
|
end
|
984
661
|
|
985
|
-
# Add the dataset to the list of compounds
|
986
|
-
def compound_clone(type, dataset, opts)
|
987
|
-
ds = compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, opts[:all]]])
|
988
|
-
opts[:from_self] == false ? ds : ds.from_self
|
989
|
-
end
|
990
|
-
|
991
662
|
# The alias to use for datasets, takes a number to make sure the name is unique.
|
992
663
|
def dataset_alias(number)
|
993
664
|
:"#{DATASET_ALIAS_BASE_NAME}#{number}"
|
@@ -1009,40 +680,6 @@ module Sequel
|
|
1009
680
|
requires_sql_standard_datetimes? ? STANDARD_TIMESTAMP_FORMAT : TIMESTAMP_FORMAT
|
1010
681
|
end
|
1011
682
|
|
1012
|
-
# SQL fragment based on the expr type. See #filter.
|
1013
|
-
def filter_expr(expr = nil, &block)
|
1014
|
-
expr = nil if expr == []
|
1015
|
-
if expr && block
|
1016
|
-
return SQL::BooleanExpression.new(:AND, filter_expr(expr), filter_expr(block))
|
1017
|
-
elsif block
|
1018
|
-
expr = block
|
1019
|
-
end
|
1020
|
-
case expr
|
1021
|
-
when Hash
|
1022
|
-
SQL::BooleanExpression.from_value_pairs(expr)
|
1023
|
-
when Array
|
1024
|
-
if String === expr[0]
|
1025
|
-
SQL::PlaceholderLiteralString.new(expr.shift, expr, true)
|
1026
|
-
elsif Sequel.condition_specifier?(expr)
|
1027
|
-
SQL::BooleanExpression.from_value_pairs(expr)
|
1028
|
-
else
|
1029
|
-
SQL::BooleanExpression.new(:AND, *expr.map{|x| filter_expr(x)})
|
1030
|
-
end
|
1031
|
-
when Proc
|
1032
|
-
filter_expr(Sequel.virtual_row(&expr))
|
1033
|
-
when SQL::NumericExpression, SQL::StringExpression
|
1034
|
-
raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
|
1035
|
-
when Symbol, SQL::Expression
|
1036
|
-
expr
|
1037
|
-
when TrueClass, FalseClass
|
1038
|
-
SQL::BooleanExpression.new(:NOOP, expr)
|
1039
|
-
when String
|
1040
|
-
LiteralString.new("(#{expr})")
|
1041
|
-
else
|
1042
|
-
raise(Error, 'Invalid filter argument')
|
1043
|
-
end
|
1044
|
-
end
|
1045
|
-
|
1046
683
|
# Format the timestamp based on the default_timestamp_format, with a couple
|
1047
684
|
# of modifiers. First, allow %N to be used for fractions seconds (if the
|
1048
685
|
# database supports them), and override %z to always use a numeric offset
|
@@ -1111,25 +748,6 @@ module Sequel
|
|
1111
748
|
end
|
1112
749
|
end
|
1113
750
|
|
1114
|
-
# Inverts the given order by breaking it into a list of column references
|
1115
|
-
# and inverting them.
|
1116
|
-
#
|
1117
|
-
# dataset.invert_order([:id.desc]]) #=> [:id]
|
1118
|
-
# dataset.invert_order(:category, :price.desc]) #=>
|
1119
|
-
# [:category.desc, :price]
|
1120
|
-
def invert_order(order)
|
1121
|
-
return nil unless order
|
1122
|
-
new_order = []
|
1123
|
-
order.map do |f|
|
1124
|
-
case f
|
1125
|
-
when SQL::OrderedExpression
|
1126
|
-
f.invert
|
1127
|
-
else
|
1128
|
-
SQL::OrderedExpression.new(f)
|
1129
|
-
end
|
1130
|
-
end
|
1131
|
-
end
|
1132
|
-
|
1133
751
|
# SQL fragment specifying a JOIN type, converts underscores to
|
1134
752
|
# spaces and upcases.
|
1135
753
|
def join_type_sql(join_type)
|
@@ -1192,11 +810,18 @@ module Sequel
|
|
1192
810
|
v.to_s
|
1193
811
|
end
|
1194
812
|
|
1195
|
-
# SQL
|
1196
|
-
#
|
1197
|
-
#
|
813
|
+
# SQL fragment for a type of object not handled by Dataset#literal.
|
814
|
+
# Calls sql_literal if object responds to it, otherwise raises an error.
|
815
|
+
# Classes implementing sql_literal should call a class-specific method on the dataset
|
816
|
+
# provided and should add that method to Sequel::Dataset, allowing for adapters
|
817
|
+
# to provide customized literalizations.
|
818
|
+
# If a database specific type is allowed, this should be overriden in a subclass.
|
1198
819
|
def literal_other(v)
|
1199
|
-
|
820
|
+
if v.respond_to?(:sql_literal)
|
821
|
+
v.sql_literal(self)
|
822
|
+
else
|
823
|
+
raise Error, "can't express #{v.inspect} as a SQL literal"
|
824
|
+
end
|
1200
825
|
end
|
1201
826
|
|
1202
827
|
# SQL fragment for String. Doubles \ and ' by default.
|
@@ -1293,7 +918,14 @@ module Sequel
|
|
1293
918
|
o[:order] = qualified_expression(o[:order], table) if o[:order]
|
1294
919
|
SQL::Window.new(o)
|
1295
920
|
when SQL::PlaceholderLiteralString
|
1296
|
-
|
921
|
+
args = if e.args.is_a?(Hash)
|
922
|
+
h = {}
|
923
|
+
e.args.each{|k,v| h[k] = qualified_expression(v, table)}
|
924
|
+
h
|
925
|
+
else
|
926
|
+
qualified_expression(e.args, table)
|
927
|
+
end
|
928
|
+
SQL::PlaceholderLiteralString.new(e.str, args, e.parens)
|
1297
929
|
else
|
1298
930
|
e
|
1299
931
|
end
|