sequel 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/CHANGELOG +108 -0
  2. data/README.rdoc +25 -14
  3. data/Rakefile +20 -1
  4. data/doc/advanced_associations.rdoc +61 -64
  5. data/doc/cheat_sheet.rdoc +16 -7
  6. data/doc/opening_databases.rdoc +3 -3
  7. data/doc/prepared_statements.rdoc +1 -1
  8. data/doc/reflection.rdoc +2 -1
  9. data/doc/release_notes/3.6.0.txt +366 -0
  10. data/doc/schema.rdoc +19 -14
  11. data/lib/sequel/adapters/amalgalite.rb +5 -27
  12. data/lib/sequel/adapters/jdbc.rb +13 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +17 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
  15. data/lib/sequel/adapters/mysql.rb +4 -3
  16. data/lib/sequel/adapters/oracle.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +87 -28
  18. data/lib/sequel/adapters/shared/mssql.rb +47 -6
  19. data/lib/sequel/adapters/shared/mysql.rb +12 -31
  20. data/lib/sequel/adapters/shared/postgres.rb +15 -12
  21. data/lib/sequel/adapters/shared/sqlite.rb +18 -0
  22. data/lib/sequel/adapters/sqlite.rb +1 -16
  23. data/lib/sequel/connection_pool.rb +1 -1
  24. data/lib/sequel/core.rb +1 -1
  25. data/lib/sequel/database.rb +1 -1
  26. data/lib/sequel/database/schema_generator.rb +2 -0
  27. data/lib/sequel/database/schema_sql.rb +1 -1
  28. data/lib/sequel/dataset.rb +5 -179
  29. data/lib/sequel/dataset/actions.rb +123 -0
  30. data/lib/sequel/dataset/convenience.rb +18 -10
  31. data/lib/sequel/dataset/features.rb +65 -0
  32. data/lib/sequel/dataset/prepared_statements.rb +29 -23
  33. data/lib/sequel/dataset/query.rb +429 -0
  34. data/lib/sequel/dataset/sql.rb +67 -435
  35. data/lib/sequel/model/associations.rb +77 -13
  36. data/lib/sequel/model/base.rb +30 -8
  37. data/lib/sequel/model/errors.rb +4 -4
  38. data/lib/sequel/plugins/caching.rb +38 -15
  39. data/lib/sequel/plugins/force_encoding.rb +18 -7
  40. data/lib/sequel/plugins/hook_class_methods.rb +4 -0
  41. data/lib/sequel/plugins/many_through_many.rb +1 -1
  42. data/lib/sequel/plugins/nested_attributes.rb +40 -11
  43. data/lib/sequel/plugins/serialization.rb +17 -3
  44. data/lib/sequel/plugins/validation_helpers.rb +65 -18
  45. data/lib/sequel/sql.rb +23 -1
  46. data/lib/sequel/version.rb +1 -1
  47. data/spec/adapters/mssql_spec.rb +96 -10
  48. data/spec/adapters/mysql_spec.rb +19 -0
  49. data/spec/adapters/postgres_spec.rb +65 -2
  50. data/spec/adapters/sqlite_spec.rb +10 -0
  51. data/spec/core/core_sql_spec.rb +9 -0
  52. data/spec/core/database_spec.rb +8 -4
  53. data/spec/core/dataset_spec.rb +122 -29
  54. data/spec/core/expression_filters_spec.rb +17 -0
  55. data/spec/extensions/caching_spec.rb +43 -3
  56. data/spec/extensions/force_encoding_spec.rb +43 -1
  57. data/spec/extensions/nested_attributes_spec.rb +55 -2
  58. data/spec/extensions/validation_helpers_spec.rb +71 -0
  59. data/spec/integration/associations_test.rb +281 -0
  60. data/spec/integration/dataset_test.rb +383 -9
  61. data/spec/integration/eager_loader_test.rb +0 -65
  62. data/spec/integration/model_test.rb +110 -0
  63. data/spec/integration/plugin_test.rb +306 -0
  64. data/spec/integration/prepared_statement_test.rb +32 -0
  65. data/spec/integration/schema_test.rb +8 -3
  66. data/spec/integration/spec_helper.rb +1 -25
  67. data/spec/model/association_reflection_spec.rb +38 -0
  68. data/spec/model/associations_spec.rb +184 -8
  69. data/spec/model/eager_loading_spec.rb +23 -0
  70. data/spec/model/model_spec.rb +8 -0
  71. data/spec/model/record_spec.rb +84 -1
  72. metadata +9 -2
@@ -0,0 +1,123 @@
1
+ module Sequel
2
+ class Dataset
3
+
4
+ # Alias for insert, but not aliased directly so subclasses
5
+ # don't have to override both methods.
6
+ def <<(*args)
7
+ insert(*args)
8
+ end
9
+
10
+ # Returns an array with all records in the dataset. If a block is given,
11
+ # the array is iterated over after all items have been loaded.
12
+ def all(&block)
13
+ a = []
14
+ each{|r| a << r}
15
+ post_load(a)
16
+ a.each(&block) if block
17
+ a
18
+ end
19
+
20
+ # Returns the columns in the result set in order.
21
+ # If the columns are currently cached, returns the cached value. Otherwise,
22
+ # a SELECT query is performed to get a single row. Adapters are expected
23
+ # to fill the columns cache with the column information when a query is performed.
24
+ # If the dataset does not have any rows, this may be an empty array depending on how
25
+ # the adapter is programmed.
26
+ #
27
+ # If you are looking for all columns for a single table and maybe some information about
28
+ # each column (e.g. type), see Database#schema.
29
+ def columns
30
+ return @columns if @columns
31
+ ds = unfiltered.unordered.clone(:distinct => nil, :limit => 1)
32
+ ds.each{break}
33
+ @columns = ds.instance_variable_get(:@columns)
34
+ @columns || []
35
+ end
36
+
37
+ # Remove the cached list of columns and do a SELECT query to find
38
+ # the columns.
39
+ def columns!
40
+ @columns = nil
41
+ columns
42
+ end
43
+
44
+ # Deletes the records in the dataset. The returned value is generally the
45
+ # number of records deleted, but that is adapter dependent. See delete_sql.
46
+ def delete
47
+ execute_dui(delete_sql)
48
+ end
49
+
50
+ # Iterates over the records in the dataset as they are yielded from the
51
+ # database adapter, and returns self.
52
+ #
53
+ # Note that this method is not safe to use on many adapters if you are
54
+ # running additional queries inside the provided block. If you are
55
+ # running queries inside the block, you use should all instead of each.
56
+ def each(&block)
57
+ if @opts[:graph]
58
+ graph_each(&block)
59
+ else
60
+ if row_proc = @row_proc
61
+ fetch_rows(select_sql){|r| yield row_proc.call(r)}
62
+ else
63
+ fetch_rows(select_sql, &block)
64
+ end
65
+ end
66
+ self
67
+ end
68
+
69
+ # Executes a select query and fetches records, passing each record to the
70
+ # supplied block. The yielded records should be hashes with symbol keys.
71
+ def fetch_rows(sql, &block)
72
+ raise NotImplementedError, NOTIMPL_MSG
73
+ end
74
+
75
+ # Inserts values into the associated table. The returned value is generally
76
+ # the value of the primary key for the inserted row, but that is adapter dependent.
77
+ # See insert_sql.
78
+ def insert(*values)
79
+ execute_insert(insert_sql(*values))
80
+ end
81
+
82
+ # Alias for set, but not aliased directly so subclasses
83
+ # don't have to override both methods.
84
+ def set(*args)
85
+ update(*args)
86
+ end
87
+
88
+ # Truncates the dataset. Returns nil.
89
+ def truncate
90
+ execute_ddl(truncate_sql)
91
+ end
92
+
93
+ # Updates values for the dataset. The returned value is generally the
94
+ # number of rows updated, but that is adapter dependent. See update_sql.
95
+ def update(values={})
96
+ execute_dui(update_sql(values))
97
+ end
98
+
99
+ private
100
+
101
+ # Execute the given SQL on the database using execute.
102
+ def execute(sql, opts={}, &block)
103
+ @db.execute(sql, {:server=>@opts[:server] || :read_only}.merge(opts), &block)
104
+ end
105
+
106
+ # Execute the given SQL on the database using execute_ddl.
107
+ def execute_ddl(sql, opts={}, &block)
108
+ @db.execute_ddl(sql, default_server_opts(opts), &block)
109
+ nil
110
+ end
111
+
112
+ # Execute the given SQL on the database using execute_dui.
113
+ def execute_dui(sql, opts={}, &block)
114
+ @db.execute_dui(sql, default_server_opts(opts), &block)
115
+ end
116
+
117
+ # Execute the given SQL on the database using execute_insert.
118
+ def execute_insert(sql, opts={}, &block)
119
+ @db.execute_insert(sql, default_server_opts(opts), &block)
120
+ end
121
+
122
+ end
123
+ end
@@ -25,7 +25,7 @@ module Sequel
25
25
 
26
26
  # Returns the average value for the given column.
27
27
  def avg(column)
28
- get{|o| o.avg(column)}
28
+ aggregate_dataset.get{avg(column)}
29
29
  end
30
30
 
31
31
  # Returns true if no records exist in the dataset, false otherwise
@@ -82,12 +82,20 @@ module Sequel
82
82
  end
83
83
 
84
84
  # Returns a dataset grouped by the given column with count by group,
85
- # order by the count of records. Examples:
85
+ # order by the count of records (in ascending order). Column aliases
86
+ # may be supplied, and will be included in the select clause.
86
87
  #
87
- # ds.group_and_count(:name) => [{:name=>'a', :count=>1}, ...]
88
- # ds.group_and_count(:first_name, :last_name) => [{:first_name=>'a', :last_name=>'b', :count=>1}, ...]
88
+ # Examples:
89
+ #
90
+ # ds.group_and_count(:name).all => [{:name=>'a', :count=>1}, ...]
91
+ # ds.group_and_count(:first_name, :last_name).all => [{:first_name=>'a', :last_name=>'b', :count=>1}, ...]
92
+ # ds.group_and_count(:first_name___name).all => [{:name=>'a', :count=>1}, ...]
89
93
  def group_and_count(*columns)
90
- group(*columns).select(*(columns + [COUNT_OF_ALL_AS_COUNT])).order(:count)
94
+ groups = columns.map do |c|
95
+ c_table, column, _ = split_symbol(c)
96
+ c_table ? column.to_sym.qualify(c_table) : column.to_sym
97
+ end
98
+ group(*groups).select(*(columns + [COUNT_OF_ALL_AS_COUNT])).order(:count)
91
99
  end
92
100
 
93
101
  # Inserts multiple records into the associated table. This method can be
@@ -130,7 +138,7 @@ module Sequel
130
138
  # Returns the interval between minimum and maximum values for the given
131
139
  # column.
132
140
  def interval(column)
133
- get{|o| o.max(column) - o.min(column)}
141
+ aggregate_dataset.get{max(column) - min(column)}
134
142
  end
135
143
 
136
144
  # Reverses the order and then runs first. Note that this
@@ -159,12 +167,12 @@ module Sequel
159
167
 
160
168
  # Returns the maximum value for the given column.
161
169
  def max(column)
162
- get{|o| o.max(column)}
170
+ aggregate_dataset.get{max(column)}
163
171
  end
164
172
 
165
173
  # Returns the minimum value for the given column.
166
174
  def min(column)
167
- get{|o| o.min(column)}
175
+ aggregate_dataset.get{min(column)}
168
176
  end
169
177
 
170
178
  # This is a front end for import that allows you to submit an array of
@@ -186,7 +194,7 @@ module Sequel
186
194
  # Returns a Range object made from the minimum and maximum values for the
187
195
  # given column.
188
196
  def range(column)
189
- if r = select{|o| [o.min(column).as(:v1), o.max(column).as(:v2)]}.first
197
+ if r = aggregate_dataset.select{[min(column).as(v1), max(column).as(v2)]}.first
190
198
  (r[:v1]..r[:v2])
191
199
  end
192
200
  end
@@ -207,7 +215,7 @@ module Sequel
207
215
 
208
216
  # Returns the sum for the given column.
209
217
  def sum(column)
210
- get{|o| o.sum(column)}
218
+ aggregate_dataset.get{sum(column)}
211
219
  end
212
220
 
213
221
  # Returns a string in CSV format containing the dataset records. By
@@ -0,0 +1,65 @@
1
+ module Sequel
2
+ class Dataset
3
+ # Whether this dataset quotes identifiers.
4
+ def quote_identifiers?
5
+ @quote_identifiers
6
+ end
7
+
8
+ # Whether the dataset requires SQL standard datetimes (false by default,
9
+ # as most allow strings with ISO 8601 format.
10
+ def requires_sql_standard_datetimes?
11
+ false
12
+ end
13
+
14
+ # Whether the dataset supports common table expressions (the WITH clause).
15
+ def supports_cte?
16
+ select_clause_methods.include?(WITH_SUPPORTED)
17
+ end
18
+
19
+ # Whether the dataset supports the DISTINCT ON clause, true by default.
20
+ def supports_distinct_on?
21
+ true
22
+ end
23
+
24
+ # Whether the dataset supports the INTERSECT and EXCEPT compound operations, true by default.
25
+ def supports_intersect_except?
26
+ true
27
+ end
28
+
29
+ # Whether the dataset supports the INTERSECT ALL and EXCEPT ALL compound operations, true by default.
30
+ def supports_intersect_except_all?
31
+ true
32
+ end
33
+
34
+ # Whether the dataset supports the IS TRUE syntax.
35
+ def supports_is_true?
36
+ true
37
+ end
38
+
39
+ # Whether the dataset supports the JOIN table USING (column1, ...) syntax.
40
+ def supports_join_using?
41
+ true
42
+ end
43
+
44
+ # Whether the IN/NOT IN operators support multiple columns when an
45
+ # array of values is given.
46
+ def supports_multiple_column_in?
47
+ true
48
+ end
49
+
50
+ # Whether the dataset supports timezones in literal timestamps
51
+ def supports_timestamp_timezones?
52
+ false
53
+ end
54
+
55
+ # Whether the dataset supports fractional seconds in literal timestamps
56
+ def supports_timestamp_usecs?
57
+ true
58
+ end
59
+
60
+ # Whether the dataset supports window functions.
61
+ def supports_window_functions?
62
+ false
63
+ end
64
+ end
65
+ end
@@ -16,11 +16,10 @@ module Sequel
16
16
  attr_accessor :bind_arguments
17
17
 
18
18
  # Set the bind arguments based on the hash and call super.
19
- def call(hash, &block)
20
- ds = clone
19
+ def call(bind_vars={}, &block)
20
+ ds = bind(bind_vars)
21
21
  ds.prepared_sql
22
- ds.bind_arguments = ds.map_to_prepared_args(hash)
23
- ds.prepared_args = hash
22
+ ds.bind_arguments = ds.map_to_prepared_args(ds.opts[:bind_vars])
24
23
  ds.run(&block)
25
24
  end
26
25
 
@@ -55,7 +54,7 @@ module Sequel
55
54
  # :insert, :update, or :delete
56
55
  attr_accessor :prepared_type
57
56
 
58
- # The bind variable hash to use when substituting
57
+ # The array/hash of bound variable placeholder names.
59
58
  attr_accessor :prepared_args
60
59
 
61
60
  # The argument to supply to insert and update, which may use
@@ -64,10 +63,8 @@ module Sequel
64
63
 
65
64
  # Sets the prepared_args to the given hash and runs the
66
65
  # prepared statement.
67
- def call(hash, &block)
68
- ds = clone
69
- ds.prepared_args = hash
70
- ds.run(&block)
66
+ def call(bind_vars={}, &block)
67
+ bind(bind_vars).run(&block)
71
68
  end
72
69
 
73
70
  # Returns the SQL for the prepared statement, depending on
@@ -79,9 +76,9 @@ module Sequel
79
76
  when :first
80
77
  clone(:limit=>1).select_sql
81
78
  when :insert
82
- insert_sql(@prepared_modify_values)
79
+ insert_sql(*@prepared_modify_values)
83
80
  when :update
84
- update_sql(@prepared_modify_values)
81
+ update_sql(*@prepared_modify_values)
85
82
  when :delete
86
83
  delete_sql
87
84
  end
@@ -91,8 +88,9 @@ module Sequel
91
88
  # prepared_args is present. If so, they are considered placeholders,
92
89
  # and they are substituted using prepared_arg.
93
90
  def literal_symbol(v)
94
- if match = PLACEHOLDER_RE.match(v.to_s) and @prepared_args
95
- literal(prepared_arg(match[1].to_sym))
91
+ if @opts[:bind_vars] and match = PLACEHOLDER_RE.match(v.to_s)
92
+ v2 = prepared_arg(match[1].to_sym)
93
+ v2 ? literal(v2) : v
96
94
  else
97
95
  super
98
96
  end
@@ -117,9 +115,9 @@ module Sequel
117
115
  when :first
118
116
  first
119
117
  when :insert
120
- insert(@prepared_modify_values)
118
+ insert(*@prepared_modify_values)
121
119
  when :update
122
- update(@prepared_modify_values)
120
+ update(*@prepared_modify_values)
123
121
  when :delete
124
122
  delete
125
123
  end
@@ -129,7 +127,7 @@ module Sequel
129
127
 
130
128
  # Returns the value of the prepared_args hash for the given key.
131
129
  def prepared_arg(k)
132
- @prepared_args[k]
130
+ @opts[:bind_vars][k]
133
131
  end
134
132
 
135
133
  # Use a clone of the dataset extended with prepared statement
@@ -137,6 +135,7 @@ module Sequel
137
135
  # bind variables/prepared arguments in subselects.
138
136
  def subselect_sql(ds)
139
137
  ps = ds.prepare(:select)
138
+ ps = ps.bind(@opts[:bind_vars]) if @opts[:bind_vars]
140
139
  ps.prepared_args = prepared_args
141
140
  ps.prepared_sql
142
141
  end
@@ -154,8 +153,8 @@ module Sequel
154
153
  # Returns a single output array mapping the values of the input hash.
155
154
  # Keys in the input hash that are used more than once in the query
156
155
  # have multiple entries in the output array.
157
- def map_to_prepared_args(hash)
158
- @prepared_args.map{|v| hash[v]}
156
+ def map_to_prepared_args(bind_vars)
157
+ prepared_args.map{|v| bind_vars[v]}
159
158
  end
160
159
 
161
160
  private
@@ -163,18 +162,25 @@ module Sequel
163
162
  # Associates the argument with name k with the next position in
164
163
  # the output array.
165
164
  def prepared_arg(k)
166
- @prepared_args << k
165
+ prepared_args << k
167
166
  prepared_arg_placeholder
168
167
  end
169
168
  end
170
169
 
170
+ # Set the bind variables to use for the call. If bind variables have
171
+ # already been set for this dataset, they are updated with the contents
172
+ # of bind_vars.
173
+ def bind(bind_vars={})
174
+ clone(:bind_vars=>@opts[:bind_vars] ? @opts[:bind_vars].merge(bind_vars) : bind_vars)
175
+ end
176
+
171
177
  # For the given type (:select, :insert, :update, or :delete),
172
178
  # run the sql with the bind variables
173
179
  # specified in the hash. values is a hash of passed to
174
180
  # insert or update (if one of those types is used),
175
181
  # which may contain placeholders.
176
- def call(type, bind_variables={}, values=nil)
177
- prepare(type, nil, values).call(bind_variables)
182
+ def call(type, bind_variables={}, *values, &block)
183
+ prepare(type, nil, *values).call(bind_variables, &block)
178
184
  end
179
185
 
180
186
  # Prepare an SQL statement for later execution. This returns
@@ -186,7 +192,7 @@ module Sequel
186
192
  # ps = prepare(:select, :select_by_name)
187
193
  # ps.call(:name=>'Blah')
188
194
  # db.call(:select_by_name, :name=>'Blah')
189
- def prepare(type, name=nil, values=nil)
195
+ def prepare(type, name=nil, *values)
190
196
  ps = to_prepared_statement(type, values)
191
197
  db.prepared_statements[name] = ps if name
192
198
  ps
@@ -197,7 +203,7 @@ module Sequel
197
203
  # Return a cloned copy of the current dataset extended with
198
204
  # PreparedStatementMethods, setting the type and modify values.
199
205
  def to_prepared_statement(type, values=nil)
200
- ps = clone
206
+ ps = bind
201
207
  ps.extend(PreparedStatementMethods)
202
208
  ps.prepared_type = type
203
209
  ps.prepared_modify_values = values
@@ -0,0 +1,429 @@
1
+ module Sequel
2
+ class Dataset
3
+
4
+ FROM_SELF_KEEP_OPTS = [:graph, :eager_graph, :graph_aliases]
5
+
6
+ # Adds an further filter to an existing filter using AND. If no filter
7
+ # exists an error is raised. This method is identical to #filter except
8
+ # it expects an existing filter.
9
+ #
10
+ # ds.filter(:a).and(:b) # SQL: WHERE a AND b
11
+ def and(*cond, &block)
12
+ raise(InvalidOperation, "No existing filter found.") unless @opts[:having] || @opts[:where]
13
+ filter(*cond, &block)
14
+ end
15
+
16
+ # Returns a copy of the dataset with the SQL DISTINCT clause.
17
+ # The DISTINCT clause is used to remove duplicate rows from the
18
+ # output. If arguments are provided, uses a DISTINCT ON clause,
19
+ # in which case it will only be distinct on those columns, instead
20
+ # of all returned columns. Raises an error if arguments
21
+ # are given and DISTINCT ON is not supported.
22
+ #
23
+ # dataset.distinct # SQL: SELECT DISTINCT * FROM items
24
+ # dataset.order(:id).distinct(:id) # SQL: SELECT DISTINCT ON (id) * FROM items ORDER BY id
25
+ def distinct(*args)
26
+ raise(InvalidOperation, "DISTINCT ON not supported") if !args.empty? && !supports_distinct_on?
27
+ clone(:distinct => args)
28
+ end
29
+
30
+ # Adds an EXCEPT clause using a second dataset object.
31
+ # An EXCEPT compound dataset returns all rows in the current dataset
32
+ # that are not in the given dataset.
33
+ # Raises an InvalidOperation if the operation is not supported.
34
+ # Options:
35
+ # * :all - Set to true to use EXCEPT ALL instead of EXCEPT, so duplicate rows can occur
36
+ # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
37
+ #
38
+ # DB[:items].except(DB[:other_items]).sql
39
+ # #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
40
+ def except(dataset, opts={})
41
+ opts = {:all=>opts} unless opts.is_a?(Hash)
42
+ raise(InvalidOperation, "EXCEPT not supported") unless supports_intersect_except?
43
+ raise(InvalidOperation, "EXCEPT ALL not supported") if opts[:all] && !supports_intersect_except_all?
44
+ compound_clone(:except, dataset, opts)
45
+ end
46
+
47
+ # Performs the inverse of Dataset#filter.
48
+ #
49
+ # dataset.exclude(:category => 'software').sql #=>
50
+ # "SELECT * FROM items WHERE (category != 'software')"
51
+ def exclude(*cond, &block)
52
+ clause = (@opts[:having] ? :having : :where)
53
+ cond = cond.first if cond.size == 1
54
+ cond = filter_expr(cond, &block)
55
+ cond = SQL::BooleanExpression.invert(cond)
56
+ cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
57
+ clone(clause => cond)
58
+ end
59
+
60
+ # Returns a copy of the dataset with the given conditions imposed upon it.
61
+ # If the query already has a HAVING clause, then the conditions are imposed in the
62
+ # HAVING clause. If not, then they are imposed in the WHERE clause.
63
+ #
64
+ # filter accepts the following argument types:
65
+ #
66
+ # * Hash - list of equality/inclusion expressions
67
+ # * Array - depends:
68
+ # * If first member is a string, assumes the rest of the arguments
69
+ # are parameters and interpolates them into the string.
70
+ # * If all members are arrays of length two, treats the same way
71
+ # as a hash, except it allows for duplicate keys to be
72
+ # specified.
73
+ # * String - taken literally
74
+ # * Symbol - taken as a boolean column argument (e.g. WHERE active)
75
+ # * Sequel::SQL::BooleanExpression - an existing condition expression,
76
+ # probably created using the Sequel expression filter DSL.
77
+ #
78
+ # filter also takes a block, which should return one of the above argument
79
+ # types, and is treated the same way. This block yields a virtual row object,
80
+ # which is easy to use to create identifiers and functions.
81
+ #
82
+ # If both a block and regular argument
83
+ # are provided, they get ANDed together.
84
+ #
85
+ # Examples:
86
+ #
87
+ # dataset.filter(:id => 3).sql #=>
88
+ # "SELECT * FROM items WHERE (id = 3)"
89
+ # dataset.filter('price < ?', 100).sql #=>
90
+ # "SELECT * FROM items WHERE price < 100"
91
+ # dataset.filter([[:id, (1,2,3)], [:id, 0..10]]).sql #=>
92
+ # "SELECT * FROM items WHERE ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10)))"
93
+ # dataset.filter('price < 100').sql #=>
94
+ # "SELECT * FROM items WHERE price < 100"
95
+ # dataset.filter(:active).sql #=>
96
+ # "SELECT * FROM items WHERE :active
97
+ # dataset.filter{|o| o.price < 100}.sql #=>
98
+ # "SELECT * FROM items WHERE (price < 100)"
99
+ #
100
+ # Multiple filter calls can be chained for scoping:
101
+ #
102
+ # software = dataset.filter(:category => 'software')
103
+ # software.filter{|o| o.price < 100}.sql #=>
104
+ # "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
105
+ #
106
+ # See doc/dataset_filtering.rdoc for more examples and details.
107
+ def filter(*cond, &block)
108
+ _filter(@opts[:having] ? :having : :where, *cond, &block)
109
+ end
110
+
111
+ # Returns a copy of the dataset with the source changed.
112
+ #
113
+ # dataset.from # SQL: SELECT *
114
+ # dataset.from(:blah) # SQL: SELECT * FROM blah
115
+ # dataset.from(:blah, :foo) # SQL: SELECT * FROM blah, foo
116
+ def from(*source)
117
+ table_alias_num = 0
118
+ sources = []
119
+ source.each do |s|
120
+ case s
121
+ when Hash
122
+ s.each{|k,v| sources << SQL::AliasedExpression.new(k,v)}
123
+ when Dataset
124
+ sources << SQL::AliasedExpression.new(s, dataset_alias(table_alias_num+=1))
125
+ when Symbol
126
+ sch, table, aliaz = split_symbol(s)
127
+ if aliaz
128
+ s = sch ? SQL::QualifiedIdentifier.new(sch.to_sym, table.to_sym) : SQL::Identifier.new(table.to_sym)
129
+ sources << SQL::AliasedExpression.new(s, aliaz.to_sym)
130
+ else
131
+ sources << s
132
+ end
133
+ else
134
+ sources << s
135
+ end
136
+ end
137
+ o = {:from=>sources.empty? ? nil : sources}
138
+ o[:num_dataset_sources] = table_alias_num if table_alias_num > 0
139
+ clone(o)
140
+ end
141
+
142
+ # Returns a dataset selecting from the current dataset.
143
+ # Supplying the :alias option controls the name of the result.
144
+ #
145
+ # ds = DB[:items].order(:name).select(:id, :name)
146
+ # ds.sql #=> "SELECT id,name FROM items ORDER BY name"
147
+ # ds.from_self.sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 't1'"
148
+ # ds.from_self(:alias=>:foo).sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 'foo'"
149
+ def from_self(opts={})
150
+ fs = {}
151
+ @opts.keys.each{|k| fs[k] = nil unless FROM_SELF_KEEP_OPTS.include?(k)}
152
+ clone(fs).from(opts[:alias] ? as(opts[:alias]) : self)
153
+ end
154
+
155
+ # Pattern match any of the columns to any of the terms. The terms can be
156
+ # strings (which use LIKE) or regular expressions (which are only supported
157
+ # in some databases). See Sequel::SQL::StringExpression.like. Note that the
158
+ # total number of pattern matches will be cols.length * terms.length,
159
+ # which could cause performance issues.
160
+ #
161
+ # dataset.grep(:a, '%test%') # SQL: SELECT * FROM items WHERE a LIKE '%test%'
162
+ # dataset.grep([:a, :b], %w'%test% foo') # SQL: SELECT * FROM items WHERE a LIKE '%test%' OR a LIKE 'foo' OR b LIKE '%test%' OR b LIKE 'foo'
163
+ def grep(cols, terms)
164
+ filter(SQL::BooleanExpression.new(:OR, *Array(cols).collect{|c| SQL::StringExpression.like(c, *terms)}))
165
+ end
166
+
167
+ # Returns a copy of the dataset with the results grouped by the value of
168
+ # the given columns.
169
+ #
170
+ # dataset.group(:id) # SELECT * FROM items GROUP BY id
171
+ # dataset.group(:id, :name) # SELECT * FROM items GROUP BY id, name
172
+ def group(*columns)
173
+ clone(:group => (columns.compact.empty? ? nil : columns))
174
+ end
175
+ alias group_by group
176
+
177
+ # Returns a copy of the dataset with the HAVING conditions changed. See #filter for argument types.
178
+ #
179
+ # dataset.group(:sum).having(:sum=>10) # SQL: SELECT * FROM items GROUP BY sum HAVING sum = 10
180
+ def having(*cond, &block)
181
+ _filter(:having, *cond, &block)
182
+ end
183
+
184
+ # Adds an INTERSECT clause using a second dataset object.
185
+ # An INTERSECT compound dataset returns all rows in both the current dataset
186
+ # and the given dataset.
187
+ # Raises an InvalidOperation if the operation is not supported.
188
+ # Options:
189
+ # * :all - Set to true to use INTERSECT ALL instead of INTERSECT, so duplicate rows can occur
190
+ # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
191
+ #
192
+ # DB[:items].intersect(DB[:other_items]).sql
193
+ # #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
194
+ def intersect(dataset, opts={})
195
+ opts = {:all=>opts} unless opts.is_a?(Hash)
196
+ raise(InvalidOperation, "INTERSECT not supported") unless supports_intersect_except?
197
+ raise(InvalidOperation, "INTERSECT ALL not supported") if opts[:all] && !supports_intersect_except_all?
198
+ compound_clone(:intersect, dataset, opts)
199
+ end
200
+
201
+ # Inverts the current filter
202
+ #
203
+ # dataset.filter(:category => 'software').invert.sql #=>
204
+ # "SELECT * FROM items WHERE (category != 'software')"
205
+ def invert
206
+ having, where = @opts[:having], @opts[:where]
207
+ raise(Error, "No current filter") unless having || where
208
+ o = {}
209
+ o[:having] = SQL::BooleanExpression.invert(having) if having
210
+ o[:where] = SQL::BooleanExpression.invert(where) if where
211
+ clone(o)
212
+ end
213
+
214
+ # If given an integer, the dataset will contain only the first l results.
215
+ # If given a range, it will contain only those at offsets within that
216
+ # range. If a second argument is given, it is used as an offset.
217
+ #
218
+ # dataset.limit(10) # SQL: SELECT * FROM items LIMIT 10
219
+ # dataset.limit(10, 20) # SQL: SELECT * FROM items LIMIT 10 OFFSET 20
220
+ def limit(l, o = nil)
221
+ return from_self.limit(l, o) if @opts[:sql]
222
+
223
+ if Range === l
224
+ o = l.first
225
+ l = l.last - l.first + (l.exclude_end? ? 0 : 1)
226
+ end
227
+ l = l.to_i
228
+ raise(Error, 'Limits must be greater than or equal to 1') unless l >= 1
229
+ opts = {:limit => l}
230
+ if o
231
+ o = o.to_i
232
+ raise(Error, 'Offsets must be greater than or equal to 0') unless o >= 0
233
+ opts[:offset] = o
234
+ end
235
+ clone(opts)
236
+ end
237
+
238
+ # Adds an alternate filter to an existing filter using OR. If no filter
239
+ # exists an error is raised.
240
+ #
241
+ # dataset.filter(:a).or(:b) # SQL: SELECT * FROM items WHERE a OR b
242
+ def or(*cond, &block)
243
+ clause = (@opts[:having] ? :having : :where)
244
+ raise(InvalidOperation, "No existing filter found.") unless @opts[clause]
245
+ cond = cond.first if cond.size == 1
246
+ clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(cond, &block)))
247
+ end
248
+
249
+ # Returns a copy of the dataset with the order changed. If a nil is given
250
+ # the returned dataset has no order. This can accept multiple arguments
251
+ # of varying kinds, and even SQL functions. If a block is given, it is treated
252
+ # as a virtual row block, similar to filter.
253
+ #
254
+ # ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
255
+ # ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
256
+ # ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
257
+ # ds.order(:a + :b).sql #=> 'SELECT * FROM items ORDER BY (a + b)'
258
+ # ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
259
+ # ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
260
+ # ds.order{|o| o.sum(:name)}.sql #=> 'SELECT * FROM items ORDER BY sum(name)'
261
+ # ds.order(nil).sql #=> 'SELECT * FROM items'
262
+ def order(*columns, &block)
263
+ columns += Array(Sequel.virtual_row(&block)) if block
264
+ clone(:order => (columns.compact.empty?) ? nil : columns)
265
+ end
266
+ alias_method :order_by, :order
267
+
268
+ # Returns a copy of the dataset with the order columns added
269
+ # to the existing order.
270
+ #
271
+ # ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
272
+ # ds.order(:a).order_more(:b).sql #=> 'SELECT * FROM items ORDER BY a, b'
273
+ def order_more(*columns, &block)
274
+ columns = @opts[:order] + columns if @opts[:order]
275
+ order(*columns, &block)
276
+ end
277
+
278
+ # Returns a copy of the dataset with the order reversed. If no order is
279
+ # given, the existing order is inverted.
280
+ def reverse_order(*order)
281
+ order(*invert_order(order.empty? ? @opts[:order] : order))
282
+ end
283
+ alias reverse reverse_order
284
+
285
+ # Returns a copy of the dataset with the columns selected changed
286
+ # to the given columns. This also takes a virtual row block,
287
+ # similar to filter.
288
+ #
289
+ # dataset.select(:a) # SELECT a FROM items
290
+ # dataset.select(:a, :b) # SELECT a, b FROM items
291
+ # dataset.select{|o| o.a, o.sum(:b)} # SELECT a, sum(b) FROM items
292
+ def select(*columns, &block)
293
+ columns += Array(Sequel.virtual_row(&block)) if block
294
+ m = []
295
+ columns.map do |i|
296
+ i.is_a?(Hash) ? m.concat(i.map{|k, v| SQL::AliasedExpression.new(k,v)}) : m << i
297
+ end
298
+ clone(:select => m)
299
+ end
300
+
301
+ # Returns a copy of the dataset selecting the wildcard.
302
+ #
303
+ # dataset.select(:a).select_all # SELECT * FROM items
304
+ def select_all
305
+ clone(:select => nil)
306
+ end
307
+
308
+ # Returns a copy of the dataset with the given columns added
309
+ # to the existing selected columns.
310
+ #
311
+ # dataset.select(:a).select(:b) # SELECT b FROM items
312
+ # dataset.select(:a).select_more(:b) # SELECT a, b FROM items
313
+ def select_more(*columns, &block)
314
+ columns = @opts[:select] + columns if @opts[:select]
315
+ select(*columns, &block)
316
+ end
317
+
318
+ # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
319
+ #
320
+ # dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
321
+ def unfiltered
322
+ clone(:where => nil, :having => nil)
323
+ end
324
+
325
+ # Returns a copy of the dataset with no grouping (GROUP or HAVING clause) applied.
326
+ #
327
+ # dataset.group(:a).having(:a=>1).where(:b).ungrouped # SELECT * FROM items WHERE b
328
+ def ungrouped
329
+ clone(:group => nil, :having => nil)
330
+ end
331
+
332
+ # Adds a UNION clause using a second dataset object.
333
+ # A UNION compound dataset returns all rows in either the current dataset
334
+ # or the given dataset.
335
+ # Options:
336
+ # * :all - Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
337
+ # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
338
+ #
339
+ # DB[:items].union(DB[:other_items]).sql
340
+ # #=> "SELECT * FROM items UNION SELECT * FROM other_items"
341
+ def union(dataset, opts={})
342
+ opts = {:all=>opts} unless opts.is_a?(Hash)
343
+ compound_clone(:union, dataset, opts)
344
+ end
345
+
346
+ # Returns a copy of the dataset with no limit or offset.
347
+ #
348
+ # dataset.limit(10, 20).unlimited # SELECT * FROM items
349
+ def unlimited
350
+ clone(:limit=>nil, :offset=>nil)
351
+ end
352
+
353
+ # Returns a copy of the dataset with no order.
354
+ #
355
+ # dataset.order(:a).unordered # SELECT * FROM items
356
+ def unordered
357
+ order(nil)
358
+ end
359
+
360
+ private
361
+
362
+ # Internal filter method so it works on either the having or where clauses.
363
+ def _filter(clause, *cond, &block)
364
+ cond = cond.first if cond.size == 1
365
+ cond = filter_expr(cond, &block)
366
+ cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
367
+ clone(clause => cond)
368
+ end
369
+
370
+ # Add the dataset to the list of compounds
371
+ def compound_clone(type, dataset, opts)
372
+ ds = compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, opts[:all]]])
373
+ opts[:from_self] == false ? ds : ds.from_self
374
+ end
375
+
376
+ # SQL fragment based on the expr type. See #filter.
377
+ def filter_expr(expr = nil, &block)
378
+ expr = nil if expr == []
379
+ if expr && block
380
+ return SQL::BooleanExpression.new(:AND, filter_expr(expr), filter_expr(block))
381
+ elsif block
382
+ expr = block
383
+ end
384
+ case expr
385
+ when Hash
386
+ SQL::BooleanExpression.from_value_pairs(expr)
387
+ when Array
388
+ if (sexpr = expr.at(0)).is_a?(String)
389
+ SQL::PlaceholderLiteralString.new(sexpr, expr[1..-1], true)
390
+ elsif Sequel.condition_specifier?(expr)
391
+ SQL::BooleanExpression.from_value_pairs(expr)
392
+ else
393
+ SQL::BooleanExpression.new(:AND, *expr.map{|x| filter_expr(x)})
394
+ end
395
+ when Proc
396
+ filter_expr(Sequel.virtual_row(&expr))
397
+ when SQL::NumericExpression, SQL::StringExpression
398
+ raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
399
+ when Symbol, SQL::Expression
400
+ expr
401
+ when TrueClass, FalseClass
402
+ SQL::BooleanExpression.new(:NOOP, expr)
403
+ when String
404
+ LiteralString.new("(#{expr})")
405
+ else
406
+ raise(Error, 'Invalid filter argument')
407
+ end
408
+ end
409
+
410
+ # Inverts the given order by breaking it into a list of column references
411
+ # and inverting them.
412
+ #
413
+ # dataset.invert_order([:id.desc]]) #=> [:id]
414
+ # dataset.invert_order(:category, :price.desc]) #=>
415
+ # [:category.desc, :price]
416
+ def invert_order(order)
417
+ return nil unless order
418
+ new_order = []
419
+ order.map do |f|
420
+ case f
421
+ when SQL::OrderedExpression
422
+ f.invert
423
+ else
424
+ SQL::OrderedExpression.new(f)
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end