sequel_core 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/CHANGELOG +116 -0
  2. data/COPYING +19 -19
  3. data/README +83 -32
  4. data/Rakefile +9 -20
  5. data/bin/sequel +43 -112
  6. data/doc/cheat_sheet.rdoc +225 -0
  7. data/doc/dataset_filtering.rdoc +257 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +4 -2
  9. data/lib/sequel_core/adapters/ado.rb +3 -1
  10. data/lib/sequel_core/adapters/db2.rb +4 -2
  11. data/lib/sequel_core/adapters/dbi.rb +127 -113
  12. data/lib/sequel_core/adapters/informix.rb +4 -2
  13. data/lib/sequel_core/adapters/jdbc.rb +5 -3
  14. data/lib/sequel_core/adapters/mysql.rb +112 -46
  15. data/lib/sequel_core/adapters/odbc.rb +5 -7
  16. data/lib/sequel_core/adapters/odbc_mssql.rb +12 -3
  17. data/lib/sequel_core/adapters/openbase.rb +3 -1
  18. data/lib/sequel_core/adapters/oracle.rb +11 -9
  19. data/lib/sequel_core/adapters/postgres.rb +261 -262
  20. data/lib/sequel_core/adapters/sqlite.rb +72 -22
  21. data/lib/sequel_core/connection_pool.rb +140 -73
  22. data/lib/sequel_core/core_ext.rb +201 -66
  23. data/lib/sequel_core/core_sql.rb +123 -153
  24. data/lib/sequel_core/database/schema.rb +156 -0
  25. data/lib/sequel_core/database.rb +321 -338
  26. data/lib/sequel_core/dataset/callback.rb +11 -12
  27. data/lib/sequel_core/dataset/convenience.rb +213 -240
  28. data/lib/sequel_core/dataset/pagination.rb +58 -43
  29. data/lib/sequel_core/dataset/parse_tree_sequelizer.rb +331 -0
  30. data/lib/sequel_core/dataset/query.rb +41 -0
  31. data/lib/sequel_core/dataset/schema.rb +15 -0
  32. data/lib/sequel_core/dataset/sequelizer.rb +41 -373
  33. data/lib/sequel_core/dataset/sql.rb +741 -632
  34. data/lib/sequel_core/dataset.rb +183 -168
  35. data/lib/sequel_core/deprecated.rb +1 -169
  36. data/lib/sequel_core/exceptions.rb +24 -19
  37. data/lib/sequel_core/migration.rb +44 -52
  38. data/lib/sequel_core/object_graph.rb +43 -42
  39. data/lib/sequel_core/pretty_table.rb +71 -76
  40. data/lib/sequel_core/schema/generator.rb +163 -105
  41. data/lib/sequel_core/schema/sql.rb +250 -93
  42. data/lib/sequel_core/schema.rb +2 -8
  43. data/lib/sequel_core/sql.rb +394 -0
  44. data/lib/sequel_core/worker.rb +37 -27
  45. data/lib/sequel_core.rb +99 -45
  46. data/spec/adapters/informix_spec.rb +0 -1
  47. data/spec/adapters/mysql_spec.rb +177 -124
  48. data/spec/adapters/oracle_spec.rb +0 -1
  49. data/spec/adapters/postgres_spec.rb +98 -58
  50. data/spec/adapters/sqlite_spec.rb +45 -4
  51. data/spec/blockless_filters_spec.rb +269 -0
  52. data/spec/connection_pool_spec.rb +21 -18
  53. data/spec/core_ext_spec.rb +169 -19
  54. data/spec/core_sql_spec.rb +56 -49
  55. data/spec/database_spec.rb +78 -17
  56. data/spec/dataset_spec.rb +300 -428
  57. data/spec/migration_spec.rb +1 -1
  58. data/spec/object_graph_spec.rb +5 -11
  59. data/spec/rcov.opts +1 -1
  60. data/spec/schema_generator_spec.rb +16 -4
  61. data/spec/schema_spec.rb +89 -10
  62. data/spec/sequelizer_spec.rb +56 -56
  63. data/spec/spec.opts +0 -5
  64. data/spec/spec_config.rb +7 -0
  65. data/spec/spec_config.rb.example +5 -5
  66. data/spec/spec_helper.rb +6 -0
  67. data/spec/worker_spec.rb +1 -1
  68. 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
- # The Dataset SQL module implements all the dataset methods concerned with
4
- # generating SQL statements for retrieving and manipulating records.
5
- module SQL
6
- # Adds quoting to column references. This method is just a stub and can
7
- # be overriden in adapters in order to provide correct column quoting
8
- # behavior.
9
- def quote_column_ref(name); name.to_s; end
10
-
11
- ALIASED_REGEXP = /\A(.*)\s(.*)\z/.freeze
12
- QUALIFIED_REGEXP = /\A(.*)\.(.*)\z/.freeze
13
-
14
- # Returns a qualified column name (including a table name) if the column
15
- # name isn't already qualified.
16
- def qualified_column_name(column, table)
17
- s = literal(column)
18
- if s =~ QUALIFIED_REGEXP
19
- return column
20
- else
21
- if table.is_a?(Dataset)
22
- table = :t1
23
- elsif (table =~ ALIASED_REGEXP)
24
- table = $2
25
- end
26
- Sequel::SQL::QualifiedColumnRef.new(table, column)
27
- end
28
- end
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
- WILDCARD = '*'.freeze
31
- COMMA_SEPARATOR = ", ".freeze
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
- # Converts an array of column names into a comma seperated string of
34
- # column names. If the array is empty, a wildcard (*) is returned.
35
- def column_list(columns)
36
- if columns.empty?
37
- WILDCARD
38
- else
39
- m = columns.map do |i|
40
- i.is_a?(Hash) ? i.map {|kv| "#{literal(kv[0])} AS #{kv[1]}"} : literal(i)
41
- end
42
- m.join(COMMA_SEPARATOR)
43
- end
44
- end
45
-
46
- def table_ref(t)
47
- case t
48
- when Dataset
49
- t.to_table_reference
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
- def first_source
78
- source = @opts[:from]
79
- if source.nil? || source.empty?
80
- raise Error, 'No source specified for query'
81
- end
82
- case s = source.first
83
- when Hash
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
- NULL = "NULL".freeze
91
- TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
92
- DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
93
- TRUE = "'t'".freeze
94
- FALSE = "'f'".freeze
95
-
96
- # Returns a literal representation of a value to be used as part
97
- # of an SQL expression. The stock implementation supports literalization
98
- # of String (with proper escaping to prevent SQL injections), numbers,
99
- # Symbol (as column references), Array (as a list of literalized values),
100
- # Time (as an SQL TIMESTAMP), Date (as an SQL DATE), Dataset (as a
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
- AND_SEPARATOR = " AND ".freeze
143
- QUESTION_MARK = '?'.freeze
83
+ sql = "DELETE FROM #{source_list(opts[:from])}"
144
84
 
145
- # Formats a where clause. If parenthesize is true, then the whole
146
- # generated clause will be enclosed in a set of parentheses.
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
- # Returns a copy of the dataset with the source changed.
167
- def from(*source)
168
- clone(:from => source)
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
- # Returns a dataset selecting from the current dataset.
172
- #
173
- # ds = DB[:items].order(:name)
174
- # ds.sql #=> "SELECT * FROM items ORDER BY name"
175
- # ds.from_self.sql #=> "SELECT * FROM (SELECT * FROM items ORDER BY name)"
176
- def from_self
177
- clone(:from => [self], :select => nil, :group => nil,
178
- :sql => nil, :distinct => nil, :join => nil, :where => nil,
179
- :order => nil, :having => nil, :limit => nil, :offset => nil,
180
- :union => nil)
181
- end
182
-
183
- # Returns a copy of the dataset with the selected columns changed.
184
- def select(*columns)
185
- clone(:select => columns)
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
- # Returns a copy of the dataset with additional selected columns.
189
- def select_more(*columns)
190
- if @opts[:select]
191
- clone(:select => @opts[:select] + columns)
192
- else
193
- clone(:select => columns)
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
- end
196
-
197
- # Returns a copy of the dataset selecting the wildcard.
198
- def select_all
199
- clone(:select => nil)
200
- end
201
-
202
- # Returns a copy of the dataset with the distinct option.
203
- def uniq(*args)
204
- clone(:distinct => args)
205
- end
206
- alias_method :distinct, :uniq
207
-
208
- # Returns a copy of the dataset with the order changed. If a nil is given
209
- # the returned dataset has no order. This can accept multiple arguments
210
- # of varying kinds, and even SQL functions.
211
- #
212
- # ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
213
- # ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
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
- clone(:order => order)
230
- end
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
- f.desc
292
+ "INSERT INTO #{from} VALUES (#{literal(values)})"
256
293
  end
257
294
  end
258
295
  end
259
-
260
- # Returns a copy of the dataset with no order.
261
- def unordered
262
- clone(:order => nil)
263
- end
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
- # Returns a copy of the dataset with the results grouped by the value of
266
- # the given columns
267
- def group(*columns)
268
- clone(:group => columns)
269
- end
270
-
271
- alias_method :group_by, :group
272
-
273
- # Returns a copy of the dataset with the given conditions imposed upon it.
274
- # If the query has been grouped, then the conditions are imposed in the
275
- # HAVING clause. If not, then they are imposed in the WHERE clause. Filter
276
- # accepts a Hash (formated into a list of equality expressions), an Array
277
- # (formatted ala ActiveRecord conditions), a String (taken literally), or
278
- # a block that is converted into expressions.
279
- #
280
- # dataset.filter(:id => 3).sql #=>
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
- if !@opts[clause].nil? and @opts[clause].any?
308
- l = expression_list(@opts[clause])
309
- r = expression_list(block || cond, parenthesize)
310
- clone(clause => "#{l} AND #{r}")
311
- else
312
- clone(:filter => cond, clause => expression_list(block || cond))
313
- end
314
- end
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
- # Adds an alternate filter to an existing filter using OR. If no filter
317
- # exists an error is raised.
318
- def or(*cond, &block)
319
- clause = (@opts[:having] ? :having : :where)
320
- cond = cond.first if cond.size == 1
321
- parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
322
- if @opts[clause]
323
- l = expression_list(@opts[clause])
324
- r = expression_list(block || cond, parenthesize)
325
- clone(clause => "#{l} OR #{r}")
326
- else
327
- raise Error::NoExistingFilter, "No existing filter found."
328
- end
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
- # Adds an further filter to an existing filter using AND. If no filter
332
- # exists an error is raised. This method is identical to #filter except
333
- # it expects an existing filter.
334
- def and(*cond, &block)
335
- clause = (@opts[:having] ? :having : :where)
336
- unless @opts[clause]
337
- raise Error::NoExistingFilter, "No existing filter found."
338
- end
339
- filter(*cond, &block)
340
- end
341
-
342
- # Performs the inverse of Dataset#filter.
343
- #
344
- # dataset.exclude(:category => 'software').sql #=>
345
- # "SELECT * FROM items WHERE NOT (category = 'software')"
346
- def exclude(*cond, &block)
347
- clause = (@opts[:having] ? :having : :where)
348
- cond = cond.first if cond.size == 1
349
- parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
350
- if @opts[clause]
351
- l = expression_list(@opts[clause])
352
- r = expression_list(block || cond, parenthesize)
353
- cond = "#{l} AND (NOT #{r})"
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
- # Returns a copy of the dataset with the where conditions changed. Raises
361
- # if the dataset has been grouped. See also #filter.
362
- def where(*cond, &block)
363
- filter(*cond, &block)
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
- # Returns a copy of the dataset with the having conditions changed. Raises
367
- # if the dataset has not been grouped. See also #filter
368
- def having(*cond, &block)
369
- unless @opts[:group]
370
- raise Error::InvalidOperation, "Can only specify a HAVING clause on a grouped dataset"
371
- else
372
- @opts[:having] = {}
373
- filter(*cond, &block)
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
- def grep(cols, terms)
378
- conds = [];
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
- # Adds a UNION clause using a second dataset object. If all is true the
386
- # clause used is UNION ALL, which may return duplicate rows.
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
- # Adds an INTERSECT clause using a second dataset object. If all is true
392
- # the clause used is INTERSECT ALL, which may return duplicate rows.
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
- # Adds an EXCEPT clause using a second dataset object. If all is true the
398
- # clause used is EXCEPT ALL, which may return duplicate rows.
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
- JOIN_TYPES = {
404
- :left_outer => 'LEFT OUTER JOIN'.freeze,
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
- # Returns a joined dataset with the specified join type and condition.
426
- def join_table(type, table, expr)
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
- # Returns a LEFT OUTER joined dataset.
447
- def left_outer_join(table, expr); join_table(:left_outer, table, expr); end
448
-
449
- # Returns a RIGHT OUTER joined dataset.
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
- # Formats a SELECT statement using the given options and the dataset
470
- # options.
471
- def select_sql(opts = nil)
472
- opts = opts ? @opts.merge(opts) : @opts
473
-
474
- if sql = opts[:sql]
475
- return sql
476
- end
477
-
478
- columns = opts[:select]
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
- if where = opts[:where]
497
- sql << " WHERE #{where}"
498
- end
589
+ sql
590
+ end
591
+ alias_method :sql, :select_sql
499
592
 
500
- if group = opts[:group]
501
- sql << " GROUP BY #{column_list(group)}"
502
- end
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
- if order = opts[:order]
505
- sql << " ORDER BY #{column_list(order)}"
506
- end
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
- if having = opts[:having]
509
- sql << " HAVING #{having}"
510
- end
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
- if limit = opts[:limit]
513
- sql << " LIMIT #{limit}"
514
- if offset = opts[:offset]
515
- sql << " OFFSET #{offset}"
516
- end
517
- end
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
- if union = opts[:union]
520
- sql << (opts[:union_all] ? \
521
- " UNION ALL #{union.sql}" : " UNION #{union.sql}")
522
- elsif intersect = opts[:intersect]
523
- sql << (opts[:intersect_all] ? \
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
- sql
531
- end
532
- alias_method :sql, :select_sql
633
+ # Returns a copy of the dataset with no order.
634
+ def unordered
635
+ order(nil)
636
+ end
533
637
 
534
- # Returns the SQL for formatting an insert statement with default values
535
- def insert_default_values_sql
536
- "INSERT INTO #{source_list(@opts[:from])} DEFAULT VALUES"
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
- # Formats an INSERT statement using the given values. If a hash is given,
540
- # the resulting statement includes column names. If no values are given,
541
- # the resulting statement includes a DEFAULT VALUES clause.
542
- #
543
- # dataset.insert_sql() #=> 'INSERT INTO items DEFAULT VALUES'
544
- # dataset.insert_sql(1,2,3) #=> 'INSERT INTO items VALUES (1, 2, 3)'
545
- # dataset.insert_sql(:a => 1, :b => 2) #=>
546
- # 'INSERT INTO items (a, b) VALUES (1, 2)'
547
- def insert_sql(*values)
548
- if values.empty?
549
- insert_default_values_sql
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
- values = values[0] if values.size == 1
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
- # Returns an array of insert statements for inserting multiple records.
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
- sql
637
- end
678
+ sql
679
+ end
638
680
 
639
- # Formats a DELETE statement using the given options and dataset options.
640
- #
641
- # dataset.filter {price >= 100}.delete_sql #=>
642
- # "DELETE FROM items WHERE (price >= 100)"
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
- if opts[:group]
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
- sql = "DELETE FROM #{source_list(opts[:from])}"
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
- if where = opts[:where]
655
- sql << " WHERE #{where}"
656
- end
694
+ private
657
695
 
658
- sql
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
- # Returns a table reference for use in the FROM clause. If the dataset has
662
- # only a :from option refering to a single table, only the table name is
663
- # returned. Otherwise a subquery is returned.
664
- def to_table_reference(idx = nil)
665
- if opts.keys == [:from] && opts[:from].size == 1
666
- opts[:from].first.to_s
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
- idx ? "(#{sql}) t#{idx}" : "(#{sql})"
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
- # Returns an EXISTS clause for the dataset.
673
- #
674
- # DB.select(1).where(DB[:items].exists).sql
675
- # #=> "SELECT 1 WHERE EXISTS (SELECT * FROM items)"
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
- opts = {}
689
- if l.is_a? Range
690
- lim = (l.exclude_end? ? l.last - l.first : l.last + 1 - l.first)
691
- opts = {:limit => lim, :offset=>l.first}
692
- elsif o
693
- opts = {:limit => l, :offset => o}
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
- opts = {:limit => l}
751
+ f.desc
696
752
  end
697
- clone(opts)
698
753
  end
699
-
700
- STOCK_COUNT_OPTS = {:select => ["COUNT(*)".lit], :order => nil}.freeze
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
- # Returns the number of records in the dataset.
703
- def count
704
- if @opts[:sql] || @opts[:group]
705
- from_self.count
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
- single_value(STOCK_COUNT_OPTS).to_i
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