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.
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