sequel 3.5.0 → 3.6.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 (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