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