sequel_core 1.5.1 → 2.0.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 +116 -0
- data/COPYING +19 -19
- data/README +83 -32
- data/Rakefile +9 -20
- data/bin/sequel +43 -112
- data/doc/cheat_sheet.rdoc +225 -0
- data/doc/dataset_filtering.rdoc +257 -0
- data/lib/sequel_core/adapters/adapter_skeleton.rb +4 -2
- data/lib/sequel_core/adapters/ado.rb +3 -1
- data/lib/sequel_core/adapters/db2.rb +4 -2
- data/lib/sequel_core/adapters/dbi.rb +127 -113
- data/lib/sequel_core/adapters/informix.rb +4 -2
- data/lib/sequel_core/adapters/jdbc.rb +5 -3
- data/lib/sequel_core/adapters/mysql.rb +112 -46
- data/lib/sequel_core/adapters/odbc.rb +5 -7
- data/lib/sequel_core/adapters/odbc_mssql.rb +12 -3
- data/lib/sequel_core/adapters/openbase.rb +3 -1
- data/lib/sequel_core/adapters/oracle.rb +11 -9
- data/lib/sequel_core/adapters/postgres.rb +261 -262
- data/lib/sequel_core/adapters/sqlite.rb +72 -22
- data/lib/sequel_core/connection_pool.rb +140 -73
- data/lib/sequel_core/core_ext.rb +201 -66
- data/lib/sequel_core/core_sql.rb +123 -153
- data/lib/sequel_core/database/schema.rb +156 -0
- data/lib/sequel_core/database.rb +321 -338
- data/lib/sequel_core/dataset/callback.rb +11 -12
- data/lib/sequel_core/dataset/convenience.rb +213 -240
- data/lib/sequel_core/dataset/pagination.rb +58 -43
- data/lib/sequel_core/dataset/parse_tree_sequelizer.rb +331 -0
- data/lib/sequel_core/dataset/query.rb +41 -0
- data/lib/sequel_core/dataset/schema.rb +15 -0
- data/lib/sequel_core/dataset/sequelizer.rb +41 -373
- data/lib/sequel_core/dataset/sql.rb +741 -632
- data/lib/sequel_core/dataset.rb +183 -168
- data/lib/sequel_core/deprecated.rb +1 -169
- data/lib/sequel_core/exceptions.rb +24 -19
- data/lib/sequel_core/migration.rb +44 -52
- data/lib/sequel_core/object_graph.rb +43 -42
- data/lib/sequel_core/pretty_table.rb +71 -76
- data/lib/sequel_core/schema/generator.rb +163 -105
- data/lib/sequel_core/schema/sql.rb +250 -93
- data/lib/sequel_core/schema.rb +2 -8
- data/lib/sequel_core/sql.rb +394 -0
- data/lib/sequel_core/worker.rb +37 -27
- data/lib/sequel_core.rb +99 -45
- data/spec/adapters/informix_spec.rb +0 -1
- data/spec/adapters/mysql_spec.rb +177 -124
- data/spec/adapters/oracle_spec.rb +0 -1
- data/spec/adapters/postgres_spec.rb +98 -58
- data/spec/adapters/sqlite_spec.rb +45 -4
- data/spec/blockless_filters_spec.rb +269 -0
- data/spec/connection_pool_spec.rb +21 -18
- data/spec/core_ext_spec.rb +169 -19
- data/spec/core_sql_spec.rb +56 -49
- data/spec/database_spec.rb +78 -17
- data/spec/dataset_spec.rb +300 -428
- data/spec/migration_spec.rb +1 -1
- data/spec/object_graph_spec.rb +5 -11
- data/spec/rcov.opts +1 -1
- data/spec/schema_generator_spec.rb +16 -4
- data/spec/schema_spec.rb +89 -10
- data/spec/sequelizer_spec.rb +56 -56
- data/spec/spec.opts +0 -5
- data/spec/spec_config.rb +7 -0
- data/spec/spec_config.rb.example +5 -5
- data/spec/spec_helper.rb +6 -0
- data/spec/worker_spec.rb +1 -1
- metadata +78 -63
@@ -1,712 +1,821 @@
|
|
1
|
+
# This file includes all the dataset methods concerned with
|
2
|
+
# generating SQL statements for retrieving and manipulating records.
|
3
|
+
|
1
4
|
module Sequel
|
2
5
|
class Dataset
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
6
|
+
AND_SEPARATOR = " AND ".freeze
|
7
|
+
BOOL_FALSE = "'f'".freeze
|
8
|
+
BOOL_TRUE = "'t'".freeze
|
9
|
+
COLUMN_REF_RE1 = /\A([\w ]+)__([\w ]+)___([\w ]+)\z/.freeze
|
10
|
+
COLUMN_REF_RE2 = /\A([\w ]+)___([\w ]+)\z/.freeze
|
11
|
+
COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
|
12
|
+
DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
|
13
|
+
JOIN_TYPES = {
|
14
|
+
:left_outer => 'LEFT OUTER JOIN'.freeze,
|
15
|
+
:right_outer => 'RIGHT OUTER JOIN'.freeze,
|
16
|
+
:full_outer => 'FULL OUTER JOIN'.freeze,
|
17
|
+
:inner => 'INNER JOIN'.freeze
|
18
|
+
}
|
19
|
+
N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
|
20
|
+
NULL = "NULL".freeze
|
21
|
+
QUESTION_MARK = '?'.freeze
|
22
|
+
STOCK_COUNT_OPTS = {:select => ["COUNT(*)".lit], :order => nil}.freeze
|
23
|
+
TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
|
24
|
+
TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
|
25
|
+
WILDCARD = '*'.freeze
|
26
|
+
|
27
|
+
# Adds an further filter to an existing filter using AND. If no filter
|
28
|
+
# exists an error is raised. This method is identical to #filter except
|
29
|
+
# it expects an existing filter.
|
30
|
+
def and(*cond, &block)
|
31
|
+
raise(Error::NoExistingFilter, "No existing filter found.") unless @opts[:having] || @opts[:where]
|
32
|
+
filter(*cond, &block)
|
33
|
+
end
|
29
34
|
|
30
|
-
|
31
|
-
|
35
|
+
# SQL fragment for specifying all columns in a given table.
|
36
|
+
def column_all_sql(ca)
|
37
|
+
"#{quote_identifier(ca.table)}.*"
|
38
|
+
end
|
32
39
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
when Hash
|
51
|
-
t.map {|k, v| "#{table_ref(k)} #{table_ref(v)}"}.join(COMMA_SEPARATOR)
|
52
|
-
when Symbol, String
|
53
|
-
t
|
54
|
-
else
|
55
|
-
literal(t)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Converts an array of sources names into into a comma separated list.
|
60
|
-
def source_list(source)
|
61
|
-
if source.nil? || source.empty?
|
62
|
-
raise Error, 'No source specified for query'
|
63
|
-
end
|
64
|
-
auto_alias_count = 0
|
65
|
-
m = source.map do |s|
|
66
|
-
case s
|
67
|
-
when Dataset
|
68
|
-
auto_alias_count += 1
|
69
|
-
s.to_table_reference(auto_alias_count)
|
70
|
-
else
|
71
|
-
table_ref(s)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
m.join(COMMA_SEPARATOR)
|
40
|
+
# SQL fragment for column expressions
|
41
|
+
def column_expr_sql(ce)
|
42
|
+
r = ce.r
|
43
|
+
"#{literal(ce.l)} #{ce.op}#{" #{literal(r)}" if r}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# SQL fragment for complex expressions
|
47
|
+
def complex_expression_sql(op, args)
|
48
|
+
case op
|
49
|
+
when *TWO_ARITY_OPERATORS
|
50
|
+
"(#{literal(args.at(0))} #{op} #{literal(args.at(1))})"
|
51
|
+
when *N_ARITY_OPERATORS
|
52
|
+
"(#{args.collect{|a| literal(a)}.join(" #{op} ")})"
|
53
|
+
when :NOT
|
54
|
+
"NOT #{literal(args.at(0))}"
|
55
|
+
else
|
56
|
+
raise(Sequel::Error, "invalid operator #{op}")
|
75
57
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
s.values.first
|
85
|
-
else
|
86
|
-
s
|
87
|
-
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the number of records in the dataset.
|
61
|
+
def count
|
62
|
+
if @opts[:sql] || @opts[:group]
|
63
|
+
from_self.count
|
64
|
+
else
|
65
|
+
single_value(STOCK_COUNT_OPTS).to_i
|
88
66
|
end
|
67
|
+
end
|
68
|
+
alias_method :size, :count
|
89
69
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
# subquery) and nil (AS NULL).
|
102
|
-
#
|
103
|
-
# dataset.literal("abc'def\\") #=> "'abc''def\\\\'"
|
104
|
-
# dataset.literal(:items__id) #=> "items.id"
|
105
|
-
# dataset.literal([1, 2, 3]) => "(1, 2, 3)"
|
106
|
-
# dataset.literal(DB[:items]) => "(SELECT * FROM items)"
|
107
|
-
#
|
108
|
-
# If an unsupported object is given, an exception is raised.
|
109
|
-
def literal(v)
|
110
|
-
case v
|
111
|
-
when LiteralString
|
112
|
-
v
|
113
|
-
when String
|
114
|
-
"'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
|
115
|
-
when Integer, Float
|
116
|
-
v.to_s
|
117
|
-
when BigDecimal
|
118
|
-
v.to_s("F")
|
119
|
-
when NilClass
|
120
|
-
NULL
|
121
|
-
when TrueClass
|
122
|
-
TRUE
|
123
|
-
when FalseClass
|
124
|
-
FALSE
|
125
|
-
when Symbol
|
126
|
-
v.to_column_ref(self)
|
127
|
-
when Sequel::SQL::Expression
|
128
|
-
v.to_s(self)
|
129
|
-
when Array
|
130
|
-
v.empty? ? NULL : v.map {|i| literal(i)}.join(COMMA_SEPARATOR)
|
131
|
-
when Time
|
132
|
-
v.strftime(TIMESTAMP_FORMAT)
|
133
|
-
when Date
|
134
|
-
v.strftime(DATE_FORMAT)
|
135
|
-
when Dataset
|
136
|
-
"(#{v.sql})"
|
137
|
-
else
|
138
|
-
raise Error, "can't express #{v.inspect} as a SQL literal"
|
139
|
-
end
|
70
|
+
# Formats a DELETE statement using the given options and dataset options.
|
71
|
+
#
|
72
|
+
# dataset.filter(:price >= 100).delete_sql #=>
|
73
|
+
# "DELETE FROM items WHERE (price >= 100)"
|
74
|
+
def delete_sql(opts = nil)
|
75
|
+
opts = opts ? @opts.merge(opts) : @opts
|
76
|
+
|
77
|
+
if opts[:group]
|
78
|
+
raise Error::InvalidOperation, "Grouped datasets cannot be deleted from"
|
79
|
+
elsif opts[:from].is_a?(Array) && opts[:from].size > 1
|
80
|
+
raise Error::InvalidOperation, "Joined datasets cannot be deleted from"
|
140
81
|
end
|
141
82
|
|
142
|
-
|
143
|
-
QUESTION_MARK = '?'.freeze
|
83
|
+
sql = "DELETE FROM #{source_list(opts[:from])}"
|
144
84
|
|
145
|
-
|
146
|
-
|
147
|
-
def expression_list(expr, parenthesize = false)
|
148
|
-
case expr
|
149
|
-
when Hash
|
150
|
-
parenthesize = false if expr.size == 1
|
151
|
-
fmt = expr.map {|i| compare_expr(i[0], i[1])}.join(AND_SEPARATOR)
|
152
|
-
when Array
|
153
|
-
fmt = expr.shift.gsub(QUESTION_MARK) {literal(expr.shift)}
|
154
|
-
when Proc
|
155
|
-
fmt = expr.to_sql(self)
|
156
|
-
else
|
157
|
-
# if the expression is compound, it should be parenthesized in order for
|
158
|
-
# things to be predictable (when using #or and #and.)
|
159
|
-
parenthesize |= expr =~ /\).+\(/
|
160
|
-
fmt = expr
|
161
|
-
end
|
162
|
-
parenthesize ? "(#{fmt})" : fmt
|
85
|
+
if where = opts[:where]
|
86
|
+
sql << " WHERE #{literal(where)}"
|
163
87
|
end
|
164
|
-
private :qualified_column_name, :column_list, :table_ref, :source_list, :expression_list
|
165
88
|
|
166
|
-
|
167
|
-
|
168
|
-
|
89
|
+
sql
|
90
|
+
end
|
91
|
+
|
92
|
+
# Adds an EXCEPT clause using a second dataset object. If all is true the
|
93
|
+
# clause used is EXCEPT ALL, which may return duplicate rows.
|
94
|
+
#
|
95
|
+
# DB[:items].except(DB[:other_items]).sql
|
96
|
+
# #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
|
97
|
+
def except(dataset, all = false)
|
98
|
+
clone(:except => dataset, :except_all => all)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Performs the inverse of Dataset#filter.
|
102
|
+
#
|
103
|
+
# dataset.exclude(:category => 'software').sql #=>
|
104
|
+
# "SELECT * FROM items WHERE (category != 'software')"
|
105
|
+
def exclude(*cond, &block)
|
106
|
+
clause = (@opts[:having] ? :having : :where)
|
107
|
+
cond = cond.first if cond.size == 1
|
108
|
+
cond = cond.sql_or if (Hash === cond) || ((Array === cond) && (cond.all_two_pairs?))
|
109
|
+
cond = filter_expr(block || cond)
|
110
|
+
cond = SQL::ComplexExpression === cond ? ~cond : SQL::ComplexExpression.new(:NOT, cond)
|
111
|
+
cond = SQL::ComplexExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
112
|
+
clone(clause => cond)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns an EXISTS clause for the dataset.
|
116
|
+
#
|
117
|
+
# DB.select(1).where(DB[:items].exists).sql
|
118
|
+
# #=> "SELECT 1 WHERE EXISTS (SELECT * FROM items)"
|
119
|
+
def exists(opts = nil)
|
120
|
+
"EXISTS (#{select_sql(opts)})"
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns a copy of the dataset with the given conditions imposed upon it.
|
124
|
+
# If the query has been grouped, then the conditions are imposed in the
|
125
|
+
# HAVING clause. If not, then they are imposed in the WHERE clause. Filter
|
126
|
+
#
|
127
|
+
# filter accepts the following argument types:
|
128
|
+
#
|
129
|
+
# * Hash - list of equality expressions
|
130
|
+
# * Array - depends:
|
131
|
+
# * If first member is a string, assumes the rest of the arguments
|
132
|
+
# are parameters and interpolates them into the string.
|
133
|
+
# * If all members are arrays of length two, treats the same way
|
134
|
+
# as a hash, except it allows for duplicate keys to be
|
135
|
+
# specified.
|
136
|
+
# * String - taken literally
|
137
|
+
# * Symbol - taken as a boolean column argument (e.g. WHERE active)
|
138
|
+
# * Sequel::SQL::ComplexExpression - an existing condition expression,
|
139
|
+
# probably created using the Sequel blockless filter DSL.
|
140
|
+
#
|
141
|
+
# filter also takes a block, but use of this is discouraged as it requires
|
142
|
+
# ParseTree.
|
143
|
+
#
|
144
|
+
# Examples:
|
145
|
+
#
|
146
|
+
# dataset.filter(:id => 3).sql #=>
|
147
|
+
# "SELECT * FROM items WHERE (id = 3)"
|
148
|
+
# dataset.filter('price < ?', 100).sql #=>
|
149
|
+
# "SELECT * FROM items WHERE price < 100"
|
150
|
+
# dataset.filter([[:id, (1,2,3)], [:id, 0..10]]).sql #=>
|
151
|
+
# "SELECT * FROM items WHERE ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10)))"
|
152
|
+
# dataset.filter('price < 100').sql #=>
|
153
|
+
# "SELECT * FROM items WHERE price < 100"
|
154
|
+
# dataset.filter(:active).sql #=>
|
155
|
+
# "SELECT * FROM items WHERE :active
|
156
|
+
# dataset.filter(:price < 100).sql #=>
|
157
|
+
# "SELECT * FROM items WHERE (price < 100)"
|
158
|
+
#
|
159
|
+
# Multiple filter calls can be chained for scoping:
|
160
|
+
#
|
161
|
+
# software = dataset.filter(:category => 'software')
|
162
|
+
# software.filter(price < 100).sql #=>
|
163
|
+
# "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
|
164
|
+
#
|
165
|
+
# See doc/dataset_filters.rdoc for more examples and details.
|
166
|
+
def filter(*cond, &block)
|
167
|
+
clause = (@opts[:having] ? :having : :where)
|
168
|
+
cond = cond.first if cond.size == 1
|
169
|
+
raise(Error::InvalidFilter, "Invalid filter specified. Did you mean to supply a block?") if cond === true || cond === false
|
170
|
+
cond = transform_save(cond) if @transform if cond.is_a?(Hash)
|
171
|
+
cond = filter_expr(block || cond)
|
172
|
+
cond = SQL::ComplexExpression.new(:AND, @opts[clause], cond) if @opts[clause] && !@opts[clause].blank?
|
173
|
+
clone(clause => cond)
|
174
|
+
end
|
175
|
+
alias_method :where, :filter
|
176
|
+
|
177
|
+
# The first source (primary table) for this dataset. If the dataset doesn't
|
178
|
+
# have a table, raises an error. If the table is aliased, returns the actual
|
179
|
+
# table name, not the alias.
|
180
|
+
def first_source
|
181
|
+
source = @opts[:from]
|
182
|
+
if source.nil? || source.empty?
|
183
|
+
raise Error, 'No source specified for query'
|
184
|
+
end
|
185
|
+
case s = source.first
|
186
|
+
when Hash
|
187
|
+
s.values.first
|
188
|
+
else
|
189
|
+
s
|
169
190
|
end
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns a copy of the dataset with the source changed.
|
194
|
+
def from(*source)
|
195
|
+
clone(:from => source)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns a dataset selecting from the current dataset.
|
199
|
+
#
|
200
|
+
# ds = DB[:items].order(:name)
|
201
|
+
# ds.sql #=> "SELECT * FROM items ORDER BY name"
|
202
|
+
# ds.from_self.sql #=> "SELECT * FROM (SELECT * FROM items ORDER BY name)"
|
203
|
+
def from_self
|
204
|
+
fs = {}
|
205
|
+
@opts.keys.each{|k| fs[k] = nil}
|
206
|
+
fs[:from] = [self]
|
207
|
+
clone(fs)
|
208
|
+
end
|
209
|
+
|
210
|
+
# SQL fragment specifying an SQL function call
|
211
|
+
def function_sql(f)
|
212
|
+
args = f.args
|
213
|
+
"#{f.f}#{args.empty? ? '()' : literal(args)}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Pattern match any of the columns to any of the terms. The terms can be
|
217
|
+
# strings (which use LIKE) or regular expressions (which are only supported
|
218
|
+
# in some databases). See Sequel::SQL::ComplexExpression.like. Note that the
|
219
|
+
# total number of pattern matches will be cols.length * terms.length,
|
220
|
+
# which could cause performance issues.
|
221
|
+
def grep(cols, terms)
|
222
|
+
filter(SQL::ComplexExpression.new(:OR, *Array(cols).collect{|c| SQL::ComplexExpression.like(c, *terms)}))
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns a copy of the dataset with the results grouped by the value of
|
226
|
+
# the given columns
|
227
|
+
def group(*columns)
|
228
|
+
clone(:group => columns)
|
229
|
+
end
|
230
|
+
alias_method :group_by, :group
|
231
|
+
|
232
|
+
# Returns a copy of the dataset with the having conditions changed. Raises
|
233
|
+
# an error if the dataset has not been grouped. See also #filter.
|
234
|
+
def having(*cond, &block)
|
235
|
+
raise(Error::InvalidOperation, "Can only specify a HAVING clause on a grouped dataset") unless @opts[:group]
|
236
|
+
clone(:having=>{}).filter(*cond, &block)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Inserts multiple values. If a block is given it is invoked for each
|
240
|
+
# item in the given array before inserting it. See #multi_insert as
|
241
|
+
# a possible faster version that inserts multiple records in one
|
242
|
+
# SQL statement.
|
243
|
+
def insert_multiple(array, &block)
|
244
|
+
if block
|
245
|
+
array.each {|i| insert(block[i])}
|
246
|
+
else
|
247
|
+
array.each {|i| insert(i)}
|
186
248
|
end
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
249
|
+
end
|
250
|
+
|
251
|
+
# Formats an INSERT statement using the given values. If a hash is given,
|
252
|
+
# the resulting statement includes column names. If no values are given,
|
253
|
+
# the resulting statement includes a DEFAULT VALUES clause.
|
254
|
+
#
|
255
|
+
# dataset.insert_sql() #=> 'INSERT INTO items DEFAULT VALUES'
|
256
|
+
# dataset.insert_sql(1,2,3) #=> 'INSERT INTO items VALUES (1, 2, 3)'
|
257
|
+
# dataset.insert_sql(:a => 1, :b => 2) #=>
|
258
|
+
# 'INSERT INTO items (a, b) VALUES (1, 2)'
|
259
|
+
def insert_sql(*values)
|
260
|
+
if values.empty?
|
261
|
+
insert_default_values_sql
|
262
|
+
else
|
263
|
+
values = values[0] if values.size == 1
|
264
|
+
|
265
|
+
# if hash or array with keys we need to transform the values
|
266
|
+
if @transform && (values.is_a?(Hash) || (values.is_a?(Array) && values.keys))
|
267
|
+
values = transform_save(values)
|
194
268
|
end
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
# ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
|
215
|
-
# ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
|
216
|
-
# ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
|
217
|
-
# ds.order(:arr|1).sql #=> 'SELECT * FROM items ORDER BY arr[1]'
|
218
|
-
# ds.order(nil).sql #=> 'SELECT * FROM items'
|
219
|
-
def order(*order)
|
220
|
-
clone(:order => (order == [nil]) ? nil : order)
|
221
|
-
end
|
222
|
-
alias_method :order_by, :order
|
223
|
-
|
224
|
-
# Returns a copy of the dataset with the order changed.
|
225
|
-
def order_more(*order)
|
226
|
-
if @opts[:order]
|
227
|
-
clone(:order => @opts[:order] + order)
|
269
|
+
from = source_list(@opts[:from])
|
270
|
+
|
271
|
+
case values
|
272
|
+
when Array
|
273
|
+
if values.empty?
|
274
|
+
insert_default_values_sql
|
275
|
+
else
|
276
|
+
"INSERT INTO #{from} VALUES #{literal(values)}"
|
277
|
+
end
|
278
|
+
when Hash
|
279
|
+
if values.empty?
|
280
|
+
insert_default_values_sql
|
281
|
+
else
|
282
|
+
fl, vl = [], []
|
283
|
+
values.each {|k, v| fl << literal(k.is_a?(String) ? k.to_sym : k); vl << literal(v)}
|
284
|
+
"INSERT INTO #{from} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
|
285
|
+
end
|
286
|
+
when Dataset
|
287
|
+
"INSERT INTO #{from} #{literal(values)}"
|
228
288
|
else
|
229
|
-
|
230
|
-
|
231
|
-
end
|
232
|
-
|
233
|
-
# Returns a copy of the dataset with the order reversed. If no order is
|
234
|
-
# given, the existing order is inverted.
|
235
|
-
def reverse_order(*order)
|
236
|
-
order(*invert_order(order.empty? ? @opts[:order] : order))
|
237
|
-
end
|
238
|
-
alias_method :reverse, :reverse_order
|
239
|
-
|
240
|
-
# Inverts the given order by breaking it into a list of column references
|
241
|
-
# and inverting them.
|
242
|
-
#
|
243
|
-
# dataset.invert_order([:id.desc]]) #=> [:id]
|
244
|
-
# dataset.invert_order(:category, :price.desc]) #=>
|
245
|
-
# [:category.desc, :price]
|
246
|
-
def invert_order(order)
|
247
|
-
return nil unless order
|
248
|
-
new_order = []
|
249
|
-
order.map do |f|
|
250
|
-
if f.is_a?(Sequel::SQL::ColumnExpr) && (f.op == Sequel::SQL::ColumnMethods::DESC)
|
251
|
-
f.l
|
252
|
-
elsif f.is_a?(Sequel::SQL::ColumnExpr) && (f.op == Sequel::SQL::ColumnMethods::ASC)
|
253
|
-
f.l.desc
|
289
|
+
if values.respond_to?(:values)
|
290
|
+
insert_sql(values.values)
|
254
291
|
else
|
255
|
-
|
292
|
+
"INSERT INTO #{from} VALUES (#{literal(values)})"
|
256
293
|
end
|
257
294
|
end
|
258
295
|
end
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
296
|
+
end
|
297
|
+
|
298
|
+
# Adds an INTERSECT clause using a second dataset object. If all is true
|
299
|
+
# the clause used is INTERSECT ALL, which may return duplicate rows.
|
300
|
+
#
|
301
|
+
# DB[:items].intersect(DB[:other_items]).sql
|
302
|
+
# #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
|
303
|
+
def intersect(dataset, all = false)
|
304
|
+
clone(:intersect => dataset, :intersect_all => all)
|
305
|
+
end
|
264
306
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
# "SELECT * FROM items WHERE (id = 3)"
|
282
|
-
# dataset.filter('price < ?', 100).sql #=>
|
283
|
-
# "SELECT * FROM items WHERE price < 100"
|
284
|
-
# dataset.filter('price < 100').sql #=>
|
285
|
-
# "SELECT * FROM items WHERE price < 100"
|
286
|
-
# dataset.filter {price < 100}.sql #=>
|
287
|
-
# "SELECT * FROM items WHERE (price < 100)"
|
288
|
-
#
|
289
|
-
# Multiple filter calls can be chained for scoping:
|
290
|
-
#
|
291
|
-
# software = dataset.filter(:category => 'software')
|
292
|
-
# software.filter {price < 100}.sql #=>
|
293
|
-
# "SELECT * FROM items WHERE (category = 'software') AND (price < 100)"
|
294
|
-
def filter(*cond, &block)
|
295
|
-
clause = (@opts[:having] ? :having : :where)
|
296
|
-
cond = cond.first if cond.size == 1
|
297
|
-
if cond === true || cond === false
|
298
|
-
raise Error::InvalidFilter, "Invalid filter specified. Did you mean to supply a block?"
|
299
|
-
end
|
300
|
-
|
301
|
-
if cond.is_a?(Hash)
|
302
|
-
cond = transform_save(cond) if @transform
|
303
|
-
filter = cond
|
304
|
-
end
|
305
|
-
parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
|
307
|
+
# Inverts the current filter
|
308
|
+
#
|
309
|
+
# dataset.filter(:category => 'software').invert.sql #=>
|
310
|
+
# "SELECT * FROM items WHERE (category != 'software')"
|
311
|
+
def invert
|
312
|
+
having, where = @opts[:having], @opts[:where]
|
313
|
+
raise(Error, "No current filter") unless having || where
|
314
|
+
o = {}
|
315
|
+
if having
|
316
|
+
o[:having] = SQL::ComplexExpression === having ? ~having : SQL::ComplexExpression.new(:NOT, having)
|
317
|
+
end
|
318
|
+
if where
|
319
|
+
o[:where] = SQL::ComplexExpression === where ? ~where : SQL::ComplexExpression.new(:NOT, where)
|
320
|
+
end
|
321
|
+
clone(o)
|
322
|
+
end
|
306
323
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
324
|
+
# Returns a joined dataset. Uses the following arguments:
|
325
|
+
#
|
326
|
+
# * type - The type of join to do (:inner, :left_outer, :right_outer, :full)
|
327
|
+
# * table - Depends on type:
|
328
|
+
# * Dataset - a subselect is performed with an alias of tN for some value of N
|
329
|
+
# * Model (or anything responding to :table_name) - table.table_name
|
330
|
+
# * String, Symbol: table
|
331
|
+
# * expr - Depends on type:
|
332
|
+
# * Hash, Array - Assumes key (1st arg) is column of joined table (unless already
|
333
|
+
# qualified), and value (2nd arg) is column of the last joined or primary table.
|
334
|
+
# To specify multiple conditions on a single joined table column, you must use an array.
|
335
|
+
# * Symbol - Assumed to be a column in the joined table that points to the id
|
336
|
+
# column in the last joined or primary table.
|
337
|
+
# * table_alias - the name of the table's alias when joining, necessary for joining
|
338
|
+
# to the same table more than once. No alias is used by default.
|
339
|
+
def join_table(type, table, expr=nil, table_alias=nil)
|
340
|
+
raise(Error::InvalidJoinType, "Invalid join type: #{type}") unless join_type = JOIN_TYPES[type || :inner]
|
341
|
+
|
342
|
+
table = if Dataset === table
|
343
|
+
table_alias = unless table_alias
|
344
|
+
table_alias_num = (@opts[:num_dataset_sources] || 0) + 1
|
345
|
+
"t#{table_alias_num}"
|
346
|
+
end
|
347
|
+
table.to_table_reference
|
348
|
+
else
|
349
|
+
table = table.table_name if table.respond_to?(:table_name)
|
350
|
+
table_alias ||= table
|
351
|
+
table_ref(table)
|
352
|
+
end
|
353
|
+
|
354
|
+
expr = [[expr, :id]] unless expr.is_one_of?(Hash, Array)
|
355
|
+
join_conditions = expr.collect do |k, v|
|
356
|
+
k = qualified_column_name(k, table_alias) if k.is_a?(Symbol)
|
357
|
+
v = qualified_column_name(v, @opts[:last_joined_table] || first_source) if v.is_a?(Symbol)
|
358
|
+
[k,v]
|
359
|
+
end
|
360
|
+
|
361
|
+
quoted_table_alias = quote_identifier(table_alias)
|
362
|
+
clause = "#{@opts[:join]} #{join_type} #{table}#{" #{quoted_table_alias}" if quoted_table_alias != table} ON #{literal(filter_expr(join_conditions))}"
|
363
|
+
opts = {:join => clause, :last_joined_table => table_alias}
|
364
|
+
opts[:num_dataset_sources] = table_alias_num if table_alias_num
|
365
|
+
clone(opts)
|
366
|
+
end
|
315
367
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
368
|
+
# If given an integer, the dataset will contain only the first l results.
|
369
|
+
# If given a range, it will contain only those at offsets within that
|
370
|
+
# range. If a second argument is given, it is used as an offset.
|
371
|
+
def limit(l, o = nil)
|
372
|
+
return from_self.limit(l, o) if @opts[:sql]
|
373
|
+
|
374
|
+
if Range === l
|
375
|
+
o = l.first
|
376
|
+
l = l.interval + 1
|
377
|
+
end
|
378
|
+
l = l.to_i
|
379
|
+
raise(Error, 'Limits must be greater than or equal to 1') unless l >= 1
|
380
|
+
opts = {:limit => l}
|
381
|
+
if o
|
382
|
+
o = o.to_i
|
383
|
+
raise(Error, 'Offsets must be greater than or equal to 0') unless o >= 0
|
384
|
+
opts[:offset] = o
|
385
|
+
end
|
386
|
+
clone(opts)
|
387
|
+
end
|
388
|
+
|
389
|
+
# Returns a literal representation of a value to be used as part
|
390
|
+
# of an SQL expression.
|
391
|
+
#
|
392
|
+
# dataset.literal("abc'def\\") #=> "'abc''def\\\\'"
|
393
|
+
# dataset.literal(:items__id) #=> "items.id"
|
394
|
+
# dataset.literal([1, 2, 3]) => "(1, 2, 3)"
|
395
|
+
# dataset.literal(DB[:items]) => "(SELECT * FROM items)"
|
396
|
+
# dataset.literal(:x + 1 > :y) => "((x + 1) > y)"
|
397
|
+
#
|
398
|
+
# If an unsupported object is given, an exception is raised.
|
399
|
+
def literal(v)
|
400
|
+
case v
|
401
|
+
when LiteralString
|
402
|
+
v
|
403
|
+
when String
|
404
|
+
"'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
|
405
|
+
when Integer, Float
|
406
|
+
v.to_s
|
407
|
+
when BigDecimal
|
408
|
+
v.to_s("F")
|
409
|
+
when NilClass
|
410
|
+
NULL
|
411
|
+
when TrueClass
|
412
|
+
BOOL_TRUE
|
413
|
+
when FalseClass
|
414
|
+
BOOL_FALSE
|
415
|
+
when Symbol
|
416
|
+
symbol_to_column_ref(v)
|
417
|
+
when ::Sequel::SQL::Expression
|
418
|
+
v.to_s(self)
|
419
|
+
when Array
|
420
|
+
v.all_two_pairs? ? literal(v.sql_expr) : (v.empty? ? '(NULL)' : "(#{v.collect{|i| literal(i)}.join(COMMA_SEPARATOR)})")
|
421
|
+
when Hash
|
422
|
+
literal(v.sql_expr)
|
423
|
+
when Time, DateTime
|
424
|
+
v.strftime(TIMESTAMP_FORMAT)
|
425
|
+
when Date
|
426
|
+
v.strftime(DATE_FORMAT)
|
427
|
+
when Dataset
|
428
|
+
"(#{v.sql})"
|
429
|
+
else
|
430
|
+
raise Error, "can't express #{v.inspect} as a SQL literal"
|
329
431
|
end
|
432
|
+
end
|
330
433
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
else
|
355
|
-
cond = "(NOT #{expression_list(block || cond, true)})"
|
356
|
-
end
|
357
|
-
clone(clause => cond)
|
434
|
+
# Returns an array of insert statements for inserting multiple records.
|
435
|
+
# This method is used by #multi_insert to format insert statements and
|
436
|
+
# expects a keys array and and an array of value arrays.
|
437
|
+
#
|
438
|
+
# This method should be overridden by descendants if the support
|
439
|
+
# inserting multiple records in a single SQL statement.
|
440
|
+
def multi_insert_sql(columns, values)
|
441
|
+
table = quote_identifier(@opts[:from].first)
|
442
|
+
columns = literal(columns)
|
443
|
+
values.map do |r|
|
444
|
+
"INSERT INTO #{table} #{columns} VALUES #{literal(r)}"
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
# Adds an alternate filter to an existing filter using OR. If no filter
|
449
|
+
# exists an error is raised.
|
450
|
+
def or(*cond, &block)
|
451
|
+
clause = (@opts[:having] ? :having : :where)
|
452
|
+
cond = cond.first if cond.size == 1
|
453
|
+
if @opts[clause]
|
454
|
+
clone(clause => SQL::ComplexExpression.new(:OR, @opts[clause], filter_expr(block || cond)))
|
455
|
+
else
|
456
|
+
raise Error::NoExistingFilter, "No existing filter found."
|
358
457
|
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Returns a copy of the dataset with the order changed. If a nil is given
|
461
|
+
# the returned dataset has no order. This can accept multiple arguments
|
462
|
+
# of varying kinds, and even SQL functions.
|
463
|
+
#
|
464
|
+
# ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
|
465
|
+
# ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
|
466
|
+
# ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
|
467
|
+
# ds.order(:a + :b).sql #=> 'SELECT * FROM items ORDER BY (a + b)'
|
468
|
+
# ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
|
469
|
+
# ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
|
470
|
+
# ds.order(:arr|1).sql #=> 'SELECT * FROM items ORDER BY arr[1]'
|
471
|
+
# ds.order(nil).sql #=> 'SELECT * FROM items'
|
472
|
+
def order(*order)
|
473
|
+
clone(:order => (order.compact.empty?) ? nil : order)
|
474
|
+
end
|
475
|
+
alias_method :order_by, :order
|
476
|
+
|
477
|
+
# Returns a copy of the dataset with the order columns added
|
478
|
+
# to the existing order.
|
479
|
+
def order_more(*order)
|
480
|
+
order(*((@opts[:order] || []) + order))
|
481
|
+
end
|
482
|
+
|
483
|
+
# SQL fragment for the qualifed column reference, specifying
|
484
|
+
# a table and a column.
|
485
|
+
def qualified_column_ref_sql(qcr)
|
486
|
+
"#{quote_identifier(qcr.table)}.#{quote_identifier(qcr.column)}"
|
487
|
+
end
|
488
|
+
|
489
|
+
# Adds quoting to identifiers (columns and tables). If identifiers are not
|
490
|
+
# being quoted, returns name as a string. If identifiers are being quoted
|
491
|
+
# quote the name with quoted_identifier.
|
492
|
+
def quote_identifier(name)
|
493
|
+
quote_identifiers? ? quoted_identifier(name) : name.to_s
|
494
|
+
end
|
495
|
+
alias_method :quote_column_ref, :quote_identifier
|
496
|
+
|
497
|
+
# This method quotes the given name with the SQL standard double quote. It
|
498
|
+
# should be overridden by subclasses to provide quoting not matching the
|
499
|
+
# SQL standard, such as backtick (used by MySQL and SQLite).
|
500
|
+
def quoted_identifier(name)
|
501
|
+
"\"#{name}\""
|
502
|
+
end
|
359
503
|
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
504
|
+
# Returns a copy of the dataset with the order reversed. If no order is
|
505
|
+
# given, the existing order is inverted.
|
506
|
+
def reverse_order(*order)
|
507
|
+
order(*invert_order(order.empty? ? @opts[:order] : order))
|
508
|
+
end
|
509
|
+
alias_method :reverse, :reverse_order
|
510
|
+
|
511
|
+
# Returns a copy of the dataset with the columns selected changed
|
512
|
+
# to the given columns.
|
513
|
+
def select(*columns)
|
514
|
+
clone(:select => columns)
|
515
|
+
end
|
516
|
+
|
517
|
+
# Returns a copy of the dataset selecting the wildcard.
|
518
|
+
def select_all
|
519
|
+
clone(:select => nil)
|
520
|
+
end
|
521
|
+
|
522
|
+
# Returns a copy of the dataset with the given columns added
|
523
|
+
# to the existing selected columns.
|
524
|
+
def select_more(*columns)
|
525
|
+
select(*((@opts[:select] || []) + columns))
|
526
|
+
end
|
527
|
+
|
528
|
+
# Formats a SELECT statement using the given options and the dataset
|
529
|
+
# options.
|
530
|
+
def select_sql(opts = nil)
|
531
|
+
opts = opts ? @opts.merge(opts) : @opts
|
532
|
+
|
533
|
+
if sql = opts[:sql]
|
534
|
+
return sql
|
364
535
|
end
|
365
536
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
end
|
537
|
+
columns = opts[:select]
|
538
|
+
select_columns = columns ? column_list(columns) : WILDCARD
|
539
|
+
|
540
|
+
if distinct = opts[:distinct]
|
541
|
+
distinct_clause = distinct.empty? ? "DISTINCT" : "DISTINCT ON (#{column_list(distinct)})"
|
542
|
+
sql = "SELECT #{distinct_clause} #{select_columns}"
|
543
|
+
else
|
544
|
+
sql = "SELECT #{select_columns}"
|
375
545
|
end
|
376
546
|
|
377
|
-
|
378
|
-
|
379
|
-
cols = [cols] unless cols.is_a?(Array)
|
380
|
-
terms = [terms] unless terms.is_a?(Array)
|
381
|
-
cols.each {|c| terms.each {|t| conds << match_expr(c, t)}}
|
382
|
-
filter(conds.join(' OR '))
|
547
|
+
if opts[:from]
|
548
|
+
sql << " FROM #{source_list(opts[:from])}"
|
383
549
|
end
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
def union(dataset, all = false)
|
388
|
-
clone(:union => dataset, :union_all => all)
|
550
|
+
|
551
|
+
if join = opts[:join]
|
552
|
+
sql << join
|
389
553
|
end
|
390
554
|
|
391
|
-
|
392
|
-
|
393
|
-
def intersect(dataset, all = false)
|
394
|
-
clone(:intersect => dataset, :intersect_all => all)
|
555
|
+
if where = opts[:where]
|
556
|
+
sql << " WHERE #{literal(where)}"
|
395
557
|
end
|
396
558
|
|
397
|
-
|
398
|
-
|
399
|
-
def except(dataset, all = false)
|
400
|
-
clone(:except => dataset, :except_all => all)
|
559
|
+
if group = opts[:group]
|
560
|
+
sql << " GROUP BY #{column_list(group)}"
|
401
561
|
end
|
402
562
|
|
403
|
-
|
404
|
-
|
405
|
-
:right_outer => 'RIGHT OUTER JOIN'.freeze,
|
406
|
-
:full_outer => 'FULL OUTER JOIN'.freeze,
|
407
|
-
:inner => 'INNER JOIN'.freeze
|
408
|
-
}
|
409
|
-
|
410
|
-
# Returns a join clause based on the specified join type and condition.
|
411
|
-
def join_expr(type, table, expr, options)
|
412
|
-
raise(Error::InvalidJoinType, "Invalid join type: #{type}") unless join_type = JOIN_TYPES[type || :inner]
|
413
|
-
|
414
|
-
table_alias = options[:table_alias]
|
415
|
-
|
416
|
-
join_conditions = {}
|
417
|
-
expr.each do |k, v|
|
418
|
-
k = qualified_column_name(k, table_alias || table) if k.is_a?(Symbol)
|
419
|
-
v = qualified_column_name(v, @opts[:last_joined_table] || first_source) if v.is_a?(Symbol)
|
420
|
-
join_conditions[k] = v
|
421
|
-
end
|
422
|
-
" #{join_type} #{table} #{"#{table_alias} " if table_alias}ON #{expression_list(join_conditions)}"
|
563
|
+
if order = opts[:order]
|
564
|
+
sql << " ORDER BY #{column_list(order)}"
|
423
565
|
end
|
424
566
|
|
425
|
-
|
426
|
-
|
427
|
-
unless expr.is_a?(Hash)
|
428
|
-
expr = {expr => :id}
|
429
|
-
end
|
430
|
-
options = {}
|
431
|
-
|
432
|
-
if Dataset === table
|
433
|
-
table = "(#{table.sql})"
|
434
|
-
table_alias_num = @opts[:num_dataset_joins] || 1
|
435
|
-
options[:table_alias] = "t#{table_alias_num}"
|
436
|
-
elsif table.respond_to?(:table_name)
|
437
|
-
table = table.table_name
|
438
|
-
end
|
439
|
-
|
440
|
-
clause = join_expr(type, table, expr, options)
|
441
|
-
opts = {:join => @opts[:join] ? @opts[:join] + clause : clause, :last_joined_table => options[:table_alias] || table}
|
442
|
-
opts[:num_dataset_joins] = table_alias_num + 1 if table_alias_num
|
443
|
-
clone(opts)
|
567
|
+
if having = opts[:having]
|
568
|
+
sql << " HAVING #{literal(having)}"
|
444
569
|
end
|
445
570
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
def right_outer_join(table, expr); join_table(:right_outer, table, expr); end
|
451
|
-
|
452
|
-
# Returns an OUTER joined dataset.
|
453
|
-
def full_outer_join(table, expr); join_table(:full_outer, table, expr); end
|
454
|
-
|
455
|
-
# Returns an INNER joined dataset.
|
456
|
-
def inner_join(table, expr); join_table(:inner, table, expr); end
|
457
|
-
alias join inner_join
|
458
|
-
|
459
|
-
# Inserts multiple values. If a block is given it is invoked for each
|
460
|
-
# item in the given array before inserting it.
|
461
|
-
def insert_multiple(array, &block)
|
462
|
-
if block
|
463
|
-
array.each {|i| insert(block[i])}
|
464
|
-
else
|
465
|
-
array.each {|i| insert(i)}
|
571
|
+
if limit = opts[:limit]
|
572
|
+
sql << " LIMIT #{limit}"
|
573
|
+
if offset = opts[:offset]
|
574
|
+
sql << " OFFSET #{offset}"
|
466
575
|
end
|
467
576
|
end
|
468
577
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
select_columns = columns ? column_list(columns) : WILDCARD
|
480
|
-
|
481
|
-
if distinct = opts[:distinct]
|
482
|
-
distinct_clause = distinct.empty? ? "DISTINCT" : "DISTINCT ON (#{column_list(distinct)})"
|
483
|
-
sql = "SELECT #{distinct_clause} #{select_columns}"
|
484
|
-
else
|
485
|
-
sql = "SELECT #{select_columns}"
|
486
|
-
end
|
487
|
-
|
488
|
-
if opts[:from]
|
489
|
-
sql << " FROM #{source_list(opts[:from])}"
|
490
|
-
end
|
491
|
-
|
492
|
-
if join = opts[:join]
|
493
|
-
sql << join
|
494
|
-
end
|
578
|
+
if union = opts[:union]
|
579
|
+
sql << (opts[:union_all] ? \
|
580
|
+
" UNION ALL #{union.sql}" : " UNION #{union.sql}")
|
581
|
+
elsif intersect = opts[:intersect]
|
582
|
+
sql << (opts[:intersect_all] ? \
|
583
|
+
" INTERSECT ALL #{intersect.sql}" : " INTERSECT #{intersect.sql}")
|
584
|
+
elsif except = opts[:except]
|
585
|
+
sql << (opts[:except_all] ? \
|
586
|
+
" EXCEPT ALL #{except.sql}" : " EXCEPT #{except.sql}")
|
587
|
+
end
|
495
588
|
|
496
|
-
|
497
|
-
|
498
|
-
|
589
|
+
sql
|
590
|
+
end
|
591
|
+
alias_method :sql, :select_sql
|
499
592
|
|
500
|
-
|
501
|
-
|
502
|
-
|
593
|
+
# SQL fragment for specifying subscripts (SQL arrays)
|
594
|
+
def subscript_sql(s)
|
595
|
+
"#{s.f}[#{s.sub.join(COMMA_SEPARATOR)}]"
|
596
|
+
end
|
503
597
|
|
504
|
-
|
505
|
-
|
506
|
-
|
598
|
+
# Converts a symbol into a column name. This method supports underscore
|
599
|
+
# notation in order to express qualified (two underscores) and aliased
|
600
|
+
# (three underscores) columns:
|
601
|
+
#
|
602
|
+
# ds = DB[:items]
|
603
|
+
# :abc.to_column_ref(ds) #=> "abc"
|
604
|
+
# :abc___a.to_column_ref(ds) #=> "abc AS a"
|
605
|
+
# :items__abc.to_column_ref(ds) #=> "items.abc"
|
606
|
+
# :items__abc___a.to_column_ref(ds) #=> "items.abc AS a"
|
607
|
+
#
|
608
|
+
def symbol_to_column_ref(sym)
|
609
|
+
c_table, column, c_alias = split_symbol(sym)
|
610
|
+
"#{"#{quote_identifier(c_table)}." if c_table}#{quote_identifier(column)}#{" AS #{quote_identifier(c_alias)}" if c_alias}"
|
611
|
+
end
|
507
612
|
|
508
|
-
|
509
|
-
|
510
|
-
|
613
|
+
# Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
|
614
|
+
def unfiltered
|
615
|
+
clone(:where => nil, :having => nil)
|
616
|
+
end
|
511
617
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
618
|
+
# Adds a UNION clause using a second dataset object. If all is true the
|
619
|
+
# clause used is UNION ALL, which may return duplicate rows.
|
620
|
+
#
|
621
|
+
# DB[:items].union(DB[:other_items]).sql
|
622
|
+
# #=> "SELECT * FROM items UNION SELECT * FROM other_items"
|
623
|
+
def union(dataset, all = false)
|
624
|
+
clone(:union => dataset, :union_all => all)
|
625
|
+
end
|
518
626
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
" INTERSECT ALL #{intersect.sql}" : " INTERSECT #{intersect.sql}")
|
525
|
-
elsif except = opts[:except]
|
526
|
-
sql << (opts[:except_all] ? \
|
527
|
-
" EXCEPT ALL #{except.sql}" : " EXCEPT #{except.sql}")
|
528
|
-
end
|
627
|
+
# Returns a copy of the dataset with the distinct option.
|
628
|
+
def uniq(*args)
|
629
|
+
clone(:distinct => args)
|
630
|
+
end
|
631
|
+
alias_method :distinct, :uniq
|
529
632
|
|
530
|
-
|
531
|
-
|
532
|
-
|
633
|
+
# Returns a copy of the dataset with no order.
|
634
|
+
def unordered
|
635
|
+
order(nil)
|
636
|
+
end
|
533
637
|
|
534
|
-
|
535
|
-
|
536
|
-
|
638
|
+
# Formats an UPDATE statement using the given values.
|
639
|
+
#
|
640
|
+
# dataset.update_sql(:price => 100, :category => 'software') #=>
|
641
|
+
# "UPDATE items SET price = 100, category = 'software'"
|
642
|
+
#
|
643
|
+
# Accepts a block, but such usage is discouraged.
|
644
|
+
#
|
645
|
+
# Raises an error if the dataset is grouped or includes more
|
646
|
+
# than one table.
|
647
|
+
def update_sql(values = {}, opts = nil, &block)
|
648
|
+
opts = opts ? @opts.merge(opts) : @opts
|
649
|
+
|
650
|
+
if opts[:group]
|
651
|
+
raise Error::InvalidOperation, "A grouped dataset cannot be updated"
|
652
|
+
elsif (opts[:from].size > 1) or opts[:join]
|
653
|
+
raise Error::InvalidOperation, "A joined dataset cannot be updated"
|
537
654
|
end
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
655
|
+
|
656
|
+
sql = "UPDATE #{source_list(@opts[:from])} SET "
|
657
|
+
if block
|
658
|
+
sql << block.to_sql(self, :comma_separated => true)
|
659
|
+
else
|
660
|
+
set = if values.is_a?(Hash)
|
661
|
+
# get values from hash
|
662
|
+
values = transform_save(values) if @transform
|
663
|
+
values.map do |k, v|
|
664
|
+
# convert string key into symbol
|
665
|
+
k = k.to_sym if String === k
|
666
|
+
"#{literal(k)} = #{literal(v)}"
|
667
|
+
end.join(COMMA_SEPARATOR)
|
550
668
|
else
|
551
|
-
|
552
|
-
|
553
|
-
# if hash or array with keys we need to transform the values
|
554
|
-
if @transform && (values.is_a?(Hash) || (values.is_a?(Array) && values.keys))
|
555
|
-
values = transform_save(values)
|
556
|
-
end
|
557
|
-
from = source_list(@opts[:from])
|
558
|
-
|
559
|
-
case values
|
560
|
-
when Array
|
561
|
-
if values.empty?
|
562
|
-
insert_default_values_sql
|
563
|
-
else
|
564
|
-
"INSERT INTO #{from} VALUES (#{literal(values)})"
|
565
|
-
end
|
566
|
-
when Hash
|
567
|
-
if values.empty?
|
568
|
-
insert_default_values_sql
|
569
|
-
else
|
570
|
-
fl, vl = [], []
|
571
|
-
values.each {|k, v| fl << literal(k.is_a?(String) ? k.to_sym : k); vl << literal(v)}
|
572
|
-
"INSERT INTO #{from} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
|
573
|
-
end
|
574
|
-
when Dataset
|
575
|
-
"INSERT INTO #{from} #{literal(values)}"
|
576
|
-
else
|
577
|
-
if values.respond_to?(:values)
|
578
|
-
insert_sql(values.values)
|
579
|
-
else
|
580
|
-
"INSERT INTO #{from} VALUES (#{literal(values)})"
|
581
|
-
end
|
582
|
-
end
|
669
|
+
# copy values verbatim
|
670
|
+
values
|
583
671
|
end
|
672
|
+
sql << set
|
584
673
|
end
|
585
|
-
|
586
|
-
|
587
|
-
# This method is used by #multi_insert to format insert statements and
|
588
|
-
# expects a keys array and and an array of value arrays.
|
589
|
-
#
|
590
|
-
# This method may be overriden by descendants.
|
591
|
-
def multi_insert_sql(columns, values)
|
592
|
-
table = @opts[:from].first
|
593
|
-
columns = literal(columns)
|
594
|
-
values.map do |r|
|
595
|
-
"INSERT INTO #{table} (#{columns}) VALUES (#{literal(r)})"
|
596
|
-
end
|
674
|
+
if where = opts[:where]
|
675
|
+
sql << " WHERE #{literal(where)}"
|
597
676
|
end
|
598
|
-
|
599
|
-
# Formats an UPDATE statement using the given values.
|
600
|
-
#
|
601
|
-
# dataset.update_sql(:price => 100, :category => 'software') #=>
|
602
|
-
# "UPDATE items SET price = 100, category = 'software'"
|
603
|
-
def update_sql(values = {}, opts = nil, &block)
|
604
|
-
opts = opts ? @opts.merge(opts) : @opts
|
605
|
-
|
606
|
-
if opts[:group]
|
607
|
-
raise Error::InvalidOperation, "A grouped dataset cannot be updated"
|
608
|
-
elsif (opts[:from].size > 1) or opts[:join]
|
609
|
-
raise Error::InvalidOperation, "A joined dataset cannot be updated"
|
610
|
-
end
|
611
|
-
|
612
|
-
sql = "UPDATE #{source_list(@opts[:from])} SET "
|
613
|
-
if block
|
614
|
-
sql << block.to_sql(self, :comma_separated => true)
|
615
|
-
else
|
616
|
-
# check if array with keys
|
617
|
-
values = values.to_hash if values.is_a?(Array) && values.keys
|
618
|
-
if values.is_a?(Hash)
|
619
|
-
# get values from hash
|
620
|
-
values = transform_save(values) if @transform
|
621
|
-
set = values.map do |k, v|
|
622
|
-
# convert string key into symbol
|
623
|
-
k = k.to_sym if String === k
|
624
|
-
"#{literal(k)} = #{literal(v)}"
|
625
|
-
end.join(COMMA_SEPARATOR)
|
626
|
-
else
|
627
|
-
# copy values verbatim
|
628
|
-
set = values
|
629
|
-
end
|
630
|
-
sql << set
|
631
|
-
end
|
632
|
-
if where = opts[:where]
|
633
|
-
sql << " WHERE #{where}"
|
634
|
-
end
|
635
677
|
|
636
|
-
|
637
|
-
|
678
|
+
sql
|
679
|
+
end
|
638
680
|
|
639
|
-
|
640
|
-
#
|
641
|
-
|
642
|
-
|
643
|
-
def delete_sql(opts = nil)
|
644
|
-
opts = opts ? @opts.merge(opts) : @opts
|
681
|
+
[:inner, :full_outer, :right_outer, :left_outer].each do |jtype|
|
682
|
+
define_method("#{jtype}_join"){|*args| join_table(jtype, *args)}
|
683
|
+
end
|
684
|
+
alias_method :join, :inner_join
|
645
685
|
|
646
|
-
|
647
|
-
raise Error::InvalidOperation, "Grouped datasets cannot be deleted from"
|
648
|
-
elsif opts[:from].is_a?(Array) && opts[:from].size > 1
|
649
|
-
raise Error::InvalidOperation, "Joined datasets cannot be deleted from"
|
650
|
-
end
|
686
|
+
protected
|
651
687
|
|
652
|
-
|
688
|
+
# Returns a table reference for use in the FROM clause. Returns an SQL subquery
|
689
|
+
# frgament with an optional table alias.
|
690
|
+
def to_table_reference(table_alias=nil)
|
691
|
+
"(#{sql})#{" #{quote_identifier(table_alias)}" if table_alias}"
|
692
|
+
end
|
653
693
|
|
654
|
-
|
655
|
-
sql << " WHERE #{where}"
|
656
|
-
end
|
694
|
+
private
|
657
695
|
|
658
|
-
|
696
|
+
# Converts an array of column names into a comma seperated string of
|
697
|
+
# column names. If the array is empty, a wildcard (*) is returned.
|
698
|
+
def column_list(columns)
|
699
|
+
if columns.empty?
|
700
|
+
WILDCARD
|
701
|
+
else
|
702
|
+
m = columns.map do |i|
|
703
|
+
i.is_a?(Hash) ? i.map{|kv| "#{literal(kv[0])} AS #{quote_identifier(kv[1])}"} : literal(i)
|
704
|
+
end
|
705
|
+
m.join(COMMA_SEPARATOR)
|
659
706
|
end
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
707
|
+
end
|
708
|
+
|
709
|
+
# SQL fragment based on the expr type. See #filter.
|
710
|
+
def filter_expr(expr)
|
711
|
+
case expr
|
712
|
+
when Hash
|
713
|
+
SQL::ComplexExpression.from_value_pairs(expr)
|
714
|
+
when Array
|
715
|
+
if String === expr[0]
|
716
|
+
filter_expr(expr.shift.gsub(QUESTION_MARK){literal(expr.shift)}.lit)
|
667
717
|
else
|
668
|
-
|
718
|
+
SQL::ComplexExpression.from_value_pairs(expr)
|
669
719
|
end
|
720
|
+
when Proc
|
721
|
+
expr.to_sql(self).lit
|
722
|
+
when Symbol, SQL::Expression
|
723
|
+
expr
|
724
|
+
when String
|
725
|
+
"(#{expr})".lit
|
726
|
+
else
|
727
|
+
raise(Sequel::Error, 'Invalid filter argument')
|
670
728
|
end
|
729
|
+
end
|
671
730
|
|
672
|
-
|
673
|
-
|
674
|
-
#
|
675
|
-
|
676
|
-
def exists(opts = nil)
|
677
|
-
"EXISTS (#{select_sql(opts)})"
|
678
|
-
end
|
679
|
-
|
680
|
-
# If given an integer, the dataset will contain only the first l results.
|
681
|
-
# If given a range, it will contain only those at offsets within that
|
682
|
-
# range. If a second argument is given, it is used as an offset.
|
683
|
-
def limit(l, o = nil)
|
684
|
-
if @opts[:sql]
|
685
|
-
return from_self.limit(l, o)
|
686
|
-
end
|
731
|
+
# SQL statement for formatting an insert statement with default values
|
732
|
+
def insert_default_values_sql
|
733
|
+
"INSERT INTO #{source_list(@opts[:from])} DEFAULT VALUES"
|
734
|
+
end
|
687
735
|
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
736
|
+
# Inverts the given order by breaking it into a list of column references
|
737
|
+
# and inverting them.
|
738
|
+
#
|
739
|
+
# dataset.invert_order([:id.desc]]) #=> [:id]
|
740
|
+
# dataset.invert_order(:category, :price.desc]) #=>
|
741
|
+
# [:category.desc, :price]
|
742
|
+
def invert_order(order)
|
743
|
+
return nil unless order
|
744
|
+
new_order = []
|
745
|
+
order.map do |f|
|
746
|
+
if f.is_a?(SQL::ColumnExpr) && (f.op == SQL::ColumnMethods::DESC)
|
747
|
+
f.l
|
748
|
+
elsif f.is_a?(SQL::ColumnExpr) && (f.op == SQL::ColumnMethods::ASC)
|
749
|
+
f.l.desc
|
694
750
|
else
|
695
|
-
|
751
|
+
f.desc
|
696
752
|
end
|
697
|
-
clone(opts)
|
698
753
|
end
|
699
|
-
|
700
|
-
|
754
|
+
end
|
755
|
+
|
756
|
+
# Returns a qualified column name (including a table name) if the column
|
757
|
+
# name isn't already qualified.
|
758
|
+
def qualified_column_name(column, table)
|
759
|
+
if Symbol === column
|
760
|
+
c_table, column, c_alias = split_symbol(column)
|
761
|
+
schema, table, t_alias = split_symbol(table) if Symbol === table
|
762
|
+
c_table ||= t_alias || table
|
763
|
+
::Sequel::SQL::QualifiedColumnRef.new(c_table, column)
|
764
|
+
else
|
765
|
+
column
|
766
|
+
end
|
767
|
+
end
|
701
768
|
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
769
|
+
# Converts an array of source names into into a comma separated list.
|
770
|
+
def source_list(source)
|
771
|
+
if source.nil? || source.empty?
|
772
|
+
raise Error, 'No source specified for query'
|
773
|
+
end
|
774
|
+
auto_alias_count = @opts[:num_dataset_sources] || 0
|
775
|
+
m = source.map do |s|
|
776
|
+
case s
|
777
|
+
when Dataset
|
778
|
+
auto_alias_count += 1
|
779
|
+
s.to_table_reference("t#{auto_alias_count}")
|
706
780
|
else
|
707
|
-
|
781
|
+
table_ref(s)
|
708
782
|
end
|
709
783
|
end
|
784
|
+
m.join(COMMA_SEPARATOR)
|
785
|
+
end
|
786
|
+
|
787
|
+
# Splits the symbol into three parts. Each part will
|
788
|
+
# either be a string or nil.
|
789
|
+
#
|
790
|
+
# For columns, these parts are the table, column, and alias.
|
791
|
+
# For tables, these parts are the schema, table, and alias.
|
792
|
+
def split_symbol(sym)
|
793
|
+
s = sym.to_s
|
794
|
+
if m = COLUMN_REF_RE1.match(s)
|
795
|
+
m[1..3]
|
796
|
+
elsif m = COLUMN_REF_RE2.match(s)
|
797
|
+
[nil, m[1], m[2]]
|
798
|
+
elsif m = COLUMN_REF_RE3.match(s)
|
799
|
+
[m[1], m[2], nil]
|
800
|
+
else
|
801
|
+
[nil, s, nil]
|
802
|
+
end
|
803
|
+
end
|
804
|
+
|
805
|
+
# SQL fragement specifying a table name.
|
806
|
+
def table_ref(t)
|
807
|
+
case t
|
808
|
+
when Dataset
|
809
|
+
t.to_table_reference
|
810
|
+
when Hash
|
811
|
+
t.map {|k, v| "#{table_ref(k)} #{table_ref(v)}"}.join(COMMA_SEPARATOR)
|
812
|
+
when Symbol
|
813
|
+
symbol_to_column_ref(t)
|
814
|
+
when String
|
815
|
+
quote_identifier(t)
|
816
|
+
else
|
817
|
+
literal(t)
|
818
|
+
end
|
710
819
|
end
|
711
820
|
end
|
712
821
|
end
|