sequel 3.5.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +108 -0
- data/README.rdoc +25 -14
- data/Rakefile +20 -1
- data/doc/advanced_associations.rdoc +61 -64
- data/doc/cheat_sheet.rdoc +16 -7
- data/doc/opening_databases.rdoc +3 -3
- data/doc/prepared_statements.rdoc +1 -1
- data/doc/reflection.rdoc +2 -1
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/schema.rdoc +19 -14
- data/lib/sequel/adapters/amalgalite.rb +5 -27
- data/lib/sequel/adapters/jdbc.rb +13 -3
- data/lib/sequel/adapters/jdbc/h2.rb +17 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
- data/lib/sequel/adapters/mysql.rb +4 -3
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +87 -28
- data/lib/sequel/adapters/shared/mssql.rb +47 -6
- data/lib/sequel/adapters/shared/mysql.rb +12 -31
- data/lib/sequel/adapters/shared/postgres.rb +15 -12
- data/lib/sequel/adapters/shared/sqlite.rb +18 -0
- data/lib/sequel/adapters/sqlite.rb +1 -16
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +2 -0
- data/lib/sequel/database/schema_sql.rb +1 -1
- data/lib/sequel/dataset.rb +5 -179
- data/lib/sequel/dataset/actions.rb +123 -0
- data/lib/sequel/dataset/convenience.rb +18 -10
- data/lib/sequel/dataset/features.rb +65 -0
- data/lib/sequel/dataset/prepared_statements.rb +29 -23
- data/lib/sequel/dataset/query.rb +429 -0
- data/lib/sequel/dataset/sql.rb +67 -435
- data/lib/sequel/model/associations.rb +77 -13
- data/lib/sequel/model/base.rb +30 -8
- data/lib/sequel/model/errors.rb +4 -4
- data/lib/sequel/plugins/caching.rb +38 -15
- data/lib/sequel/plugins/force_encoding.rb +18 -7
- data/lib/sequel/plugins/hook_class_methods.rb +4 -0
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +40 -11
- data/lib/sequel/plugins/serialization.rb +17 -3
- data/lib/sequel/plugins/validation_helpers.rb +65 -18
- data/lib/sequel/sql.rb +23 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +96 -10
- data/spec/adapters/mysql_spec.rb +19 -0
- data/spec/adapters/postgres_spec.rb +65 -2
- data/spec/adapters/sqlite_spec.rb +10 -0
- data/spec/core/core_sql_spec.rb +9 -0
- data/spec/core/database_spec.rb +8 -4
- data/spec/core/dataset_spec.rb +122 -29
- data/spec/core/expression_filters_spec.rb +17 -0
- data/spec/extensions/caching_spec.rb +43 -3
- data/spec/extensions/force_encoding_spec.rb +43 -1
- data/spec/extensions/nested_attributes_spec.rb +55 -2
- data/spec/extensions/validation_helpers_spec.rb +71 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/dataset_test.rb +383 -9
- data/spec/integration/eager_loader_test.rb +0 -65
- data/spec/integration/model_test.rb +110 -0
- data/spec/integration/plugin_test.rb +306 -0
- data/spec/integration/prepared_statement_test.rb +32 -0
- data/spec/integration/schema_test.rb +8 -3
- data/spec/integration/spec_helper.rb +1 -25
- data/spec/model/association_reflection_spec.rb +38 -0
- data/spec/model/associations_spec.rb +184 -8
- data/spec/model/eager_loading_spec.rb +23 -0
- data/spec/model/model_spec.rb +8 -0
- data/spec/model/record_spec.rb +84 -1
- 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{
|
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.
|
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
|
-
#
|
88
|
-
#
|
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
|
-
|
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{
|
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{
|
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{
|
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{
|
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{
|
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(
|
20
|
-
ds =
|
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(
|
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
|
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(
|
68
|
-
|
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(
|
79
|
+
insert_sql(*@prepared_modify_values)
|
83
80
|
when :update
|
84
|
-
update_sql(
|
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)
|
95
|
-
|
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(
|
118
|
+
insert(*@prepared_modify_values)
|
121
119
|
when :update
|
122
|
-
update(
|
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
|
-
@
|
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(
|
158
|
-
|
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
|
-
|
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
|
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
|
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 =
|
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
|