sequel 3.5.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|