viking-sequel 3.10.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 +3134 -0
- data/COPYING +19 -0
- data/README.rdoc +723 -0
- data/Rakefile +193 -0
- data/bin/sequel +196 -0
- data/doc/advanced_associations.rdoc +644 -0
- data/doc/cheat_sheet.rdoc +218 -0
- data/doc/dataset_basics.rdoc +106 -0
- data/doc/dataset_filtering.rdoc +158 -0
- data/doc/opening_databases.rdoc +296 -0
- data/doc/prepared_statements.rdoc +104 -0
- data/doc/reflection.rdoc +84 -0
- data/doc/release_notes/1.0.txt +38 -0
- data/doc/release_notes/1.1.txt +143 -0
- data/doc/release_notes/1.3.txt +101 -0
- data/doc/release_notes/1.4.0.txt +53 -0
- data/doc/release_notes/1.5.0.txt +155 -0
- data/doc/release_notes/2.0.0.txt +298 -0
- data/doc/release_notes/2.1.0.txt +271 -0
- data/doc/release_notes/2.10.0.txt +328 -0
- data/doc/release_notes/2.11.0.txt +215 -0
- data/doc/release_notes/2.12.0.txt +534 -0
- data/doc/release_notes/2.2.0.txt +253 -0
- data/doc/release_notes/2.3.0.txt +88 -0
- data/doc/release_notes/2.4.0.txt +106 -0
- data/doc/release_notes/2.5.0.txt +137 -0
- data/doc/release_notes/2.6.0.txt +157 -0
- data/doc/release_notes/2.7.0.txt +166 -0
- data/doc/release_notes/2.8.0.txt +171 -0
- data/doc/release_notes/2.9.0.txt +97 -0
- data/doc/release_notes/3.0.0.txt +221 -0
- data/doc/release_notes/3.1.0.txt +406 -0
- data/doc/release_notes/3.10.0.txt +286 -0
- data/doc/release_notes/3.2.0.txt +268 -0
- data/doc/release_notes/3.3.0.txt +192 -0
- data/doc/release_notes/3.4.0.txt +325 -0
- data/doc/release_notes/3.5.0.txt +510 -0
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/release_notes/3.7.0.txt +179 -0
- data/doc/release_notes/3.8.0.txt +151 -0
- data/doc/release_notes/3.9.0.txt +233 -0
- data/doc/schema.rdoc +36 -0
- data/doc/sharding.rdoc +113 -0
- data/doc/virtual_rows.rdoc +205 -0
- data/lib/sequel.rb +1 -0
- data/lib/sequel/adapters/ado.rb +90 -0
- data/lib/sequel/adapters/ado/mssql.rb +30 -0
- data/lib/sequel/adapters/amalgalite.rb +176 -0
- data/lib/sequel/adapters/db2.rb +139 -0
- data/lib/sequel/adapters/dbi.rb +113 -0
- data/lib/sequel/adapters/do.rb +188 -0
- data/lib/sequel/adapters/do/mysql.rb +49 -0
- data/lib/sequel/adapters/do/postgres.rb +91 -0
- data/lib/sequel/adapters/do/sqlite.rb +40 -0
- data/lib/sequel/adapters/firebird.rb +283 -0
- data/lib/sequel/adapters/informix.rb +77 -0
- data/lib/sequel/adapters/jdbc.rb +587 -0
- data/lib/sequel/adapters/jdbc/as400.rb +58 -0
- data/lib/sequel/adapters/jdbc/h2.rb +133 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +57 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +78 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +50 -0
- data/lib/sequel/adapters/jdbc/postgresql.rb +108 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +55 -0
- data/lib/sequel/adapters/mysql.rb +421 -0
- data/lib/sequel/adapters/odbc.rb +143 -0
- data/lib/sequel/adapters/odbc/mssql.rb +42 -0
- data/lib/sequel/adapters/openbase.rb +64 -0
- data/lib/sequel/adapters/oracle.rb +131 -0
- data/lib/sequel/adapters/postgres.rb +504 -0
- data/lib/sequel/adapters/shared/mssql.rb +490 -0
- data/lib/sequel/adapters/shared/mysql.rb +498 -0
- data/lib/sequel/adapters/shared/oracle.rb +195 -0
- data/lib/sequel/adapters/shared/postgres.rb +830 -0
- data/lib/sequel/adapters/shared/progress.rb +44 -0
- data/lib/sequel/adapters/shared/sqlite.rb +389 -0
- data/lib/sequel/adapters/sqlite.rb +224 -0
- data/lib/sequel/adapters/utils/stored_procedures.rb +84 -0
- data/lib/sequel/connection_pool.rb +99 -0
- data/lib/sequel/connection_pool/sharded_single.rb +84 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +211 -0
- data/lib/sequel/connection_pool/single.rb +29 -0
- data/lib/sequel/connection_pool/threaded.rb +150 -0
- data/lib/sequel/core.rb +293 -0
- data/lib/sequel/core_sql.rb +241 -0
- data/lib/sequel/database.rb +1079 -0
- data/lib/sequel/database/schema_generator.rb +327 -0
- data/lib/sequel/database/schema_methods.rb +203 -0
- data/lib/sequel/database/schema_sql.rb +320 -0
- data/lib/sequel/dataset.rb +32 -0
- data/lib/sequel/dataset/actions.rb +441 -0
- data/lib/sequel/dataset/features.rb +86 -0
- data/lib/sequel/dataset/graph.rb +254 -0
- data/lib/sequel/dataset/misc.rb +119 -0
- data/lib/sequel/dataset/mutation.rb +64 -0
- data/lib/sequel/dataset/prepared_statements.rb +227 -0
- data/lib/sequel/dataset/query.rb +709 -0
- data/lib/sequel/dataset/sql.rb +996 -0
- data/lib/sequel/exceptions.rb +51 -0
- data/lib/sequel/extensions/blank.rb +43 -0
- data/lib/sequel/extensions/inflector.rb +242 -0
- data/lib/sequel/extensions/looser_typecasting.rb +21 -0
- data/lib/sequel/extensions/migration.rb +239 -0
- data/lib/sequel/extensions/named_timezones.rb +61 -0
- data/lib/sequel/extensions/pagination.rb +100 -0
- data/lib/sequel/extensions/pretty_table.rb +82 -0
- data/lib/sequel/extensions/query.rb +52 -0
- data/lib/sequel/extensions/schema_dumper.rb +271 -0
- data/lib/sequel/extensions/sql_expr.rb +122 -0
- data/lib/sequel/extensions/string_date_time.rb +46 -0
- data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
- data/lib/sequel/metaprogramming.rb +9 -0
- data/lib/sequel/model.rb +120 -0
- data/lib/sequel/model/associations.rb +1514 -0
- data/lib/sequel/model/base.rb +1069 -0
- data/lib/sequel/model/default_inflections.rb +45 -0
- data/lib/sequel/model/errors.rb +39 -0
- data/lib/sequel/model/exceptions.rb +21 -0
- data/lib/sequel/model/inflections.rb +162 -0
- data/lib/sequel/model/plugins.rb +70 -0
- data/lib/sequel/plugins/active_model.rb +59 -0
- data/lib/sequel/plugins/association_dependencies.rb +103 -0
- data/lib/sequel/plugins/association_proxies.rb +41 -0
- data/lib/sequel/plugins/boolean_readers.rb +53 -0
- data/lib/sequel/plugins/caching.rb +141 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
- data/lib/sequel/plugins/composition.rb +138 -0
- data/lib/sequel/plugins/force_encoding.rb +72 -0
- data/lib/sequel/plugins/hook_class_methods.rb +126 -0
- data/lib/sequel/plugins/identity_map.rb +116 -0
- data/lib/sequel/plugins/instance_filters.rb +98 -0
- data/lib/sequel/plugins/instance_hooks.rb +57 -0
- data/lib/sequel/plugins/lazy_attributes.rb +77 -0
- data/lib/sequel/plugins/many_through_many.rb +208 -0
- data/lib/sequel/plugins/nested_attributes.rb +206 -0
- data/lib/sequel/plugins/optimistic_locking.rb +81 -0
- data/lib/sequel/plugins/rcte_tree.rb +281 -0
- data/lib/sequel/plugins/schema.rb +66 -0
- data/lib/sequel/plugins/serialization.rb +166 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +74 -0
- data/lib/sequel/plugins/subclasses.rb +45 -0
- data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
- data/lib/sequel/plugins/timestamps.rb +87 -0
- data/lib/sequel/plugins/touch.rb +118 -0
- data/lib/sequel/plugins/typecast_on_load.rb +72 -0
- data/lib/sequel/plugins/validation_class_methods.rb +405 -0
- data/lib/sequel/plugins/validation_helpers.rb +223 -0
- data/lib/sequel/sql.rb +1020 -0
- data/lib/sequel/timezones.rb +161 -0
- data/lib/sequel/version.rb +12 -0
- data/lib/sequel_core.rb +1 -0
- data/lib/sequel_model.rb +1 -0
- data/spec/adapters/firebird_spec.rb +407 -0
- data/spec/adapters/informix_spec.rb +97 -0
- data/spec/adapters/mssql_spec.rb +403 -0
- data/spec/adapters/mysql_spec.rb +1019 -0
- data/spec/adapters/oracle_spec.rb +286 -0
- data/spec/adapters/postgres_spec.rb +969 -0
- data/spec/adapters/spec_helper.rb +51 -0
- data/spec/adapters/sqlite_spec.rb +432 -0
- data/spec/core/connection_pool_spec.rb +808 -0
- data/spec/core/core_sql_spec.rb +417 -0
- data/spec/core/database_spec.rb +1662 -0
- data/spec/core/dataset_spec.rb +3827 -0
- data/spec/core/expression_filters_spec.rb +595 -0
- data/spec/core/object_graph_spec.rb +296 -0
- data/spec/core/schema_generator_spec.rb +159 -0
- data/spec/core/schema_spec.rb +830 -0
- data/spec/core/spec_helper.rb +56 -0
- data/spec/core/version_spec.rb +7 -0
- data/spec/extensions/active_model_spec.rb +76 -0
- data/spec/extensions/association_dependencies_spec.rb +127 -0
- data/spec/extensions/association_proxies_spec.rb +50 -0
- data/spec/extensions/blank_spec.rb +67 -0
- data/spec/extensions/boolean_readers_spec.rb +92 -0
- data/spec/extensions/caching_spec.rb +250 -0
- data/spec/extensions/class_table_inheritance_spec.rb +252 -0
- data/spec/extensions/composition_spec.rb +194 -0
- data/spec/extensions/force_encoding_spec.rb +117 -0
- data/spec/extensions/hook_class_methods_spec.rb +470 -0
- data/spec/extensions/identity_map_spec.rb +202 -0
- data/spec/extensions/inflector_spec.rb +181 -0
- data/spec/extensions/instance_filters_spec.rb +55 -0
- data/spec/extensions/instance_hooks_spec.rb +133 -0
- data/spec/extensions/lazy_attributes_spec.rb +153 -0
- data/spec/extensions/looser_typecasting_spec.rb +39 -0
- data/spec/extensions/many_through_many_spec.rb +884 -0
- data/spec/extensions/migration_spec.rb +332 -0
- data/spec/extensions/named_timezones_spec.rb +72 -0
- data/spec/extensions/nested_attributes_spec.rb +396 -0
- data/spec/extensions/optimistic_locking_spec.rb +100 -0
- data/spec/extensions/pagination_spec.rb +99 -0
- data/spec/extensions/pretty_table_spec.rb +91 -0
- data/spec/extensions/query_spec.rb +85 -0
- data/spec/extensions/rcte_tree_spec.rb +205 -0
- data/spec/extensions/schema_dumper_spec.rb +357 -0
- data/spec/extensions/schema_spec.rb +127 -0
- data/spec/extensions/serialization_spec.rb +209 -0
- data/spec/extensions/single_table_inheritance_spec.rb +96 -0
- data/spec/extensions/spec_helper.rb +91 -0
- data/spec/extensions/sql_expr_spec.rb +89 -0
- data/spec/extensions/string_date_time_spec.rb +93 -0
- data/spec/extensions/subclasses_spec.rb +52 -0
- data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
- data/spec/extensions/thread_local_timezones_spec.rb +45 -0
- data/spec/extensions/timestamps_spec.rb +150 -0
- data/spec/extensions/touch_spec.rb +155 -0
- data/spec/extensions/typecast_on_load_spec.rb +69 -0
- data/spec/extensions/validation_class_methods_spec.rb +984 -0
- data/spec/extensions/validation_helpers_spec.rb +438 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/database_test.rb +26 -0
- data/spec/integration/dataset_test.rb +963 -0
- data/spec/integration/eager_loader_test.rb +734 -0
- data/spec/integration/model_test.rb +130 -0
- data/spec/integration/plugin_test.rb +814 -0
- data/spec/integration/prepared_statement_test.rb +213 -0
- data/spec/integration/schema_test.rb +361 -0
- data/spec/integration/spec_helper.rb +73 -0
- data/spec/integration/timezone_test.rb +55 -0
- data/spec/integration/transaction_test.rb +122 -0
- data/spec/integration/type_test.rb +96 -0
- data/spec/model/association_reflection_spec.rb +175 -0
- data/spec/model/associations_spec.rb +2633 -0
- data/spec/model/base_spec.rb +418 -0
- data/spec/model/dataset_methods_spec.rb +78 -0
- data/spec/model/eager_loading_spec.rb +1391 -0
- data/spec/model/hooks_spec.rb +240 -0
- data/spec/model/inflector_spec.rb +26 -0
- data/spec/model/model_spec.rb +593 -0
- data/spec/model/plugins_spec.rb +236 -0
- data/spec/model/record_spec.rb +1500 -0
- data/spec/model/spec_helper.rb +97 -0
- data/spec/model/validations_spec.rb +153 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec_config.rb.example +10 -0
- metadata +346 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Dataset
|
3
|
+
# ---------------------
|
4
|
+
# :section: Mutation methods
|
5
|
+
# These methods modify the receiving dataset and should be used with care.
|
6
|
+
# ---------------------
|
7
|
+
|
8
|
+
# All methods that should have a ! method added that modifies
|
9
|
+
# the receiver.
|
10
|
+
MUTATION_METHODS = %w'add_graph_aliases and cross_join distinct except exclude
|
11
|
+
filter for_update from from_self full_join full_outer_join graph
|
12
|
+
group group_and_count group_by having inner_join intersect invert join join_table left_join
|
13
|
+
left_outer_join limit lock_style naked natural_full_join natural_join
|
14
|
+
natural_left_join natural_right_join or order order_by order_more paginate qualify query
|
15
|
+
reverse reverse_order right_join right_outer_join select select_all select_append select_more server
|
16
|
+
set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
|
17
|
+
unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym}
|
18
|
+
|
19
|
+
# Setup mutation (e.g. filter!) methods. These operate the same as the
|
20
|
+
# non-! methods, but replace the options of the current dataset with the
|
21
|
+
# options of the resulting dataset.
|
22
|
+
def self.def_mutation_method(*meths)
|
23
|
+
meths.each do |meth|
|
24
|
+
class_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add the mutation methods via metaprogramming
|
29
|
+
def_mutation_method(*MUTATION_METHODS)
|
30
|
+
|
31
|
+
|
32
|
+
# Set the method to call on identifiers going into the database for this dataset
|
33
|
+
attr_accessor :identifier_input_method
|
34
|
+
|
35
|
+
# Set the method to call on identifiers coming the database for this dataset
|
36
|
+
attr_accessor :identifier_output_method
|
37
|
+
|
38
|
+
# Whether to quote identifiers for this dataset
|
39
|
+
attr_writer :quote_identifiers
|
40
|
+
|
41
|
+
# The row_proc for this database, should be a Proc that takes
|
42
|
+
# a single hash argument and returns the object you want
|
43
|
+
# each to return.
|
44
|
+
attr_accessor :row_proc
|
45
|
+
|
46
|
+
# Add a mutation method to this dataset instance.
|
47
|
+
def def_mutation_method(*meths)
|
48
|
+
meths.each do |meth|
|
49
|
+
instance_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Modify the receiver with the results of sending the meth, args, and block
|
56
|
+
# to the receiver and merging the options of the resulting dataset into
|
57
|
+
# the receiver's options.
|
58
|
+
def mutation_method(meth, *args, &block)
|
59
|
+
copy = send(meth, *args, &block)
|
60
|
+
@opts.merge!(copy.opts)
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Dataset
|
3
|
+
# ---------------------
|
4
|
+
# :section: Methods related to prepared statements or bound variables
|
5
|
+
# On some adapters, these use native prepared statements and bound variables, on others
|
6
|
+
# support is emulated. For details, see the {"Prepared Statements/Bound Variables" guide}[link:files/doc/prepared_statements_rdoc.html].
|
7
|
+
# ---------------------
|
8
|
+
|
9
|
+
PREPARED_ARG_PLACEHOLDER = LiteralString.new('?').freeze
|
10
|
+
|
11
|
+
# Default implementation of the argument mapper to allow
|
12
|
+
# native database support for bind variables and prepared
|
13
|
+
# statements (as opposed to the emulated ones used by default).
|
14
|
+
module ArgumentMapper
|
15
|
+
SQL_QUERY_TYPE = Hash.new{|h,k| h[k] = k}
|
16
|
+
SQL_QUERY_TYPE[:first] = SQL_QUERY_TYPE[:all] = :select
|
17
|
+
|
18
|
+
# The name of the prepared statement, if any.
|
19
|
+
attr_accessor :prepared_statement_name
|
20
|
+
|
21
|
+
# The bind arguments to use for running this prepared statement
|
22
|
+
attr_accessor :bind_arguments
|
23
|
+
|
24
|
+
# Set the bind arguments based on the hash and call super.
|
25
|
+
def call(bind_vars={}, &block)
|
26
|
+
ds = bind(bind_vars)
|
27
|
+
ds.prepared_sql
|
28
|
+
ds.bind_arguments = ds.map_to_prepared_args(ds.opts[:bind_vars])
|
29
|
+
ds.run(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Override the given *_sql method based on the type, and
|
33
|
+
# cache the result of the sql.
|
34
|
+
def prepared_sql
|
35
|
+
return @prepared_sql if @prepared_sql
|
36
|
+
@prepared_args ||= []
|
37
|
+
@prepared_sql = super
|
38
|
+
meta_def("#{sql_query_type}_sql"){|*args| prepared_sql}
|
39
|
+
@prepared_sql
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# The type of query (:select, :insert, :delete, :update).
|
45
|
+
def sql_query_type
|
46
|
+
SQL_QUERY_TYPE[@prepared_type]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Backbone of the prepared statement support. Grafts bind variable
|
51
|
+
# support into datasets by hijacking #literal and using placeholders.
|
52
|
+
# By default, emulates prepared statements and bind variables by
|
53
|
+
# taking the hash of bind variables and directly substituting them
|
54
|
+
# into the query, which works on all databases, as it is no different
|
55
|
+
# from using the dataset without bind variables.
|
56
|
+
module PreparedStatementMethods
|
57
|
+
PLACEHOLDER_RE = /\A\$(.*)\z/
|
58
|
+
|
59
|
+
# The type of prepared statement, should be one of :select, :first,
|
60
|
+
# :insert, :update, or :delete
|
61
|
+
attr_accessor :prepared_type
|
62
|
+
|
63
|
+
# The array/hash of bound variable placeholder names.
|
64
|
+
attr_accessor :prepared_args
|
65
|
+
|
66
|
+
# The argument to supply to insert and update, which may use
|
67
|
+
# placeholders specified by prepared_args
|
68
|
+
attr_accessor :prepared_modify_values
|
69
|
+
|
70
|
+
# Sets the prepared_args to the given hash and runs the
|
71
|
+
# prepared statement.
|
72
|
+
def call(bind_vars={}, &block)
|
73
|
+
bind(bind_vars).run(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the SQL for the prepared statement, depending on
|
77
|
+
# the type of the statement and the prepared_modify_values.
|
78
|
+
def prepared_sql
|
79
|
+
case @prepared_type
|
80
|
+
when :select, :all
|
81
|
+
select_sql
|
82
|
+
when :first
|
83
|
+
clone(:limit=>1).select_sql
|
84
|
+
when :insert
|
85
|
+
insert_sql(*@prepared_modify_values)
|
86
|
+
when :update
|
87
|
+
update_sql(*@prepared_modify_values)
|
88
|
+
when :delete
|
89
|
+
delete_sql
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Changes the values of symbols if they start with $ and
|
94
|
+
# prepared_args is present. If so, they are considered placeholders,
|
95
|
+
# and they are substituted using prepared_arg.
|
96
|
+
def literal_symbol(v)
|
97
|
+
if @opts[:bind_vars] and match = PLACEHOLDER_RE.match(v.to_s)
|
98
|
+
v2 = prepared_arg(match[1].to_sym)
|
99
|
+
v2 ? literal(v2) : v
|
100
|
+
else
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Programmer friendly string showing this is a prepared statement,
|
106
|
+
# with the prepared SQL it represents (which in general won't have
|
107
|
+
# substituted variables).
|
108
|
+
def inspect
|
109
|
+
"<#{self.class.name}/PreparedStatement #{prepared_sql.inspect}>"
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
# Run the method based on the type of prepared statement, with
|
115
|
+
# :select running #all to get all of the rows, and the other
|
116
|
+
# types running the method with the same name as the type.
|
117
|
+
def run(&block)
|
118
|
+
case @prepared_type
|
119
|
+
when :select, :all
|
120
|
+
all(&block)
|
121
|
+
when :first
|
122
|
+
first
|
123
|
+
when :insert
|
124
|
+
insert(*@prepared_modify_values)
|
125
|
+
when :update
|
126
|
+
update(*@prepared_modify_values)
|
127
|
+
when :delete
|
128
|
+
delete
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# Returns the value of the prepared_args hash for the given key.
|
135
|
+
def prepared_arg(k)
|
136
|
+
@opts[:bind_vars][k]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Use a clone of the dataset extended with prepared statement
|
140
|
+
# support and using the same argument hash so that you can use
|
141
|
+
# bind variables/prepared arguments in subselects.
|
142
|
+
def subselect_sql(ds)
|
143
|
+
ps = ds.prepare(:select)
|
144
|
+
ps = ps.bind(@opts[:bind_vars]) if @opts[:bind_vars]
|
145
|
+
ps.prepared_args = prepared_args
|
146
|
+
ps.prepared_sql
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Default implementation for an argument mapper that uses
|
151
|
+
# unnumbered SQL placeholder arguments. Keeps track of which
|
152
|
+
# arguments have been used, and allows arguments to
|
153
|
+
# be used more than once.
|
154
|
+
module UnnumberedArgumentMapper
|
155
|
+
include ArgumentMapper
|
156
|
+
|
157
|
+
protected
|
158
|
+
|
159
|
+
# Returns a single output array mapping the values of the input hash.
|
160
|
+
# Keys in the input hash that are used more than once in the query
|
161
|
+
# have multiple entries in the output array.
|
162
|
+
def map_to_prepared_args(bind_vars)
|
163
|
+
prepared_args.map{|v| bind_vars[v]}
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
# Associates the argument with name k with the next position in
|
169
|
+
# the output array.
|
170
|
+
def prepared_arg(k)
|
171
|
+
prepared_args << k
|
172
|
+
prepared_arg_placeholder
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Set the bind variables to use for the call. If bind variables have
|
177
|
+
# already been set for this dataset, they are updated with the contents
|
178
|
+
# of bind_vars.
|
179
|
+
def bind(bind_vars={})
|
180
|
+
clone(:bind_vars=>@opts[:bind_vars] ? @opts[:bind_vars].merge(bind_vars) : bind_vars)
|
181
|
+
end
|
182
|
+
|
183
|
+
# For the given type (:select, :insert, :update, or :delete),
|
184
|
+
# run the sql with the bind variables
|
185
|
+
# specified in the hash. values is a hash of passed to
|
186
|
+
# insert or update (if one of those types is used),
|
187
|
+
# which may contain placeholders.
|
188
|
+
def call(type, bind_variables={}, *values, &block)
|
189
|
+
prepare(type, nil, *values).call(bind_variables, &block)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Prepare an SQL statement for later execution. This returns
|
193
|
+
# a clone of the dataset extended with PreparedStatementMethods,
|
194
|
+
# on which you can call call with the hash of bind variables to
|
195
|
+
# do substitution. The prepared statement is also stored in
|
196
|
+
# the associated database. The following usage is identical:
|
197
|
+
#
|
198
|
+
# ps = prepare(:select, :select_by_name)
|
199
|
+
# ps.call(:name=>'Blah')
|
200
|
+
# db.call(:select_by_name, :name=>'Blah')
|
201
|
+
def prepare(type, name=nil, *values)
|
202
|
+
ps = to_prepared_statement(type, values)
|
203
|
+
db.prepared_statements[name] = ps if name
|
204
|
+
ps
|
205
|
+
end
|
206
|
+
|
207
|
+
protected
|
208
|
+
|
209
|
+
# Return a cloned copy of the current dataset extended with
|
210
|
+
# PreparedStatementMethods, setting the type and modify values.
|
211
|
+
def to_prepared_statement(type, values=nil)
|
212
|
+
ps = bind
|
213
|
+
ps.extend(PreparedStatementMethods)
|
214
|
+
ps.prepared_type = type
|
215
|
+
ps.prepared_modify_values = values
|
216
|
+
ps
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
# The argument placeholder. Most databases used unnumbered
|
222
|
+
# arguments with question marks, so that is the default.
|
223
|
+
def prepared_arg_placeholder
|
224
|
+
PREPARED_ARG_PLACEHOLDER
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,709 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Dataset
|
3
|
+
# ---------------------
|
4
|
+
# :section: Methods that return modified datasets
|
5
|
+
# These methods all return modified copies of the receiver.
|
6
|
+
# ---------------------
|
7
|
+
# The dataset options that require the removal of cached columns
|
8
|
+
# if changed.
|
9
|
+
COLUMN_CHANGE_OPTS = [:select, :sql, :from, :join].freeze
|
10
|
+
|
11
|
+
# Which options don't affect the SQL generation. Used by simple_select_all?
|
12
|
+
# to determine if this is a simple SELECT * FROM table.
|
13
|
+
NON_SQL_OPTIONS = [:server, :defaults, :overrides, :graph, :eager_graph, :graph_aliases]
|
14
|
+
|
15
|
+
# These symbols have _join methods created (e.g. inner_join) that
|
16
|
+
# call join_table with the symbol, passing along the arguments and
|
17
|
+
# block from the method call.
|
18
|
+
CONDITIONED_JOIN_TYPES = [:inner, :full_outer, :right_outer, :left_outer, :full, :right, :left]
|
19
|
+
|
20
|
+
# These symbols have _join methods created (e.g. natural_join) that
|
21
|
+
# call join_table with the symbol. They only accept a single table
|
22
|
+
# argument which is passed to join_table, and they raise an error
|
23
|
+
# if called with a block.
|
24
|
+
UNCONDITIONED_JOIN_TYPES = [:natural, :natural_left, :natural_right, :natural_full, :cross]
|
25
|
+
|
26
|
+
# Adds an further filter to an existing filter using AND. If no filter
|
27
|
+
# exists an error is raised. This method is identical to #filter except
|
28
|
+
# it expects an existing filter.
|
29
|
+
#
|
30
|
+
# ds.filter(:a).and(:b) # SQL: WHERE a AND b
|
31
|
+
def and(*cond, &block)
|
32
|
+
raise(InvalidOperation, "No existing filter found.") unless @opts[:having] || @opts[:where]
|
33
|
+
filter(*cond, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a new clone of the dataset with with the given options merged.
|
37
|
+
# If the options changed include options in COLUMN_CHANGE_OPTS, the cached
|
38
|
+
# columns are deleted.
|
39
|
+
def clone(opts = {})
|
40
|
+
c = super()
|
41
|
+
c.opts = @opts.merge(opts)
|
42
|
+
c.instance_variable_set(:@columns, nil) if opts.keys.any?{|o| COLUMN_CHANGE_OPTS.include?(o)}
|
43
|
+
c
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a copy of the dataset with the SQL DISTINCT clause.
|
47
|
+
# The DISTINCT clause is used to remove duplicate rows from the
|
48
|
+
# output. If arguments are provided, uses a DISTINCT ON clause,
|
49
|
+
# in which case it will only be distinct on those columns, instead
|
50
|
+
# of all returned columns. Raises an error if arguments
|
51
|
+
# are given and DISTINCT ON is not supported.
|
52
|
+
#
|
53
|
+
# dataset.distinct # SQL: SELECT DISTINCT * FROM items
|
54
|
+
# dataset.order(:id).distinct(:id) # SQL: SELECT DISTINCT ON (id) * FROM items ORDER BY id
|
55
|
+
def distinct(*args)
|
56
|
+
raise(InvalidOperation, "DISTINCT ON not supported") if !args.empty? && !supports_distinct_on?
|
57
|
+
clone(:distinct => args)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Adds an EXCEPT clause using a second dataset object.
|
61
|
+
# An EXCEPT compound dataset returns all rows in the current dataset
|
62
|
+
# that are not in the given dataset.
|
63
|
+
# Raises an InvalidOperation if the operation is not supported.
|
64
|
+
# Options:
|
65
|
+
# * :all - Set to true to use EXCEPT ALL instead of EXCEPT, so duplicate rows can occur
|
66
|
+
# * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
|
67
|
+
#
|
68
|
+
# DB[:items].except(DB[:other_items]).sql
|
69
|
+
# #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
|
70
|
+
def except(dataset, opts={})
|
71
|
+
opts = {:all=>opts} unless opts.is_a?(Hash)
|
72
|
+
raise(InvalidOperation, "EXCEPT not supported") unless supports_intersect_except?
|
73
|
+
raise(InvalidOperation, "EXCEPT ALL not supported") if opts[:all] && !supports_intersect_except_all?
|
74
|
+
compound_clone(:except, dataset, opts)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Performs the inverse of Dataset#filter.
|
78
|
+
#
|
79
|
+
# dataset.exclude(:category => 'software').sql #=>
|
80
|
+
# "SELECT * FROM items WHERE (category != 'software')"
|
81
|
+
def exclude(*cond, &block)
|
82
|
+
clause = (@opts[:having] ? :having : :where)
|
83
|
+
cond = cond.first if cond.size == 1
|
84
|
+
cond = filter_expr(cond, &block)
|
85
|
+
cond = SQL::BooleanExpression.invert(cond)
|
86
|
+
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
87
|
+
clone(clause => cond)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns a copy of the dataset with the given conditions imposed upon it.
|
91
|
+
# If the query already has a HAVING clause, then the conditions are imposed in the
|
92
|
+
# HAVING clause. If not, then they are imposed in the WHERE clause.
|
93
|
+
#
|
94
|
+
# filter accepts the following argument types:
|
95
|
+
#
|
96
|
+
# * Hash - list of equality/inclusion expressions
|
97
|
+
# * Array - depends:
|
98
|
+
# * If first member is a string, assumes the rest of the arguments
|
99
|
+
# are parameters and interpolates them into the string.
|
100
|
+
# * If all members are arrays of length two, treats the same way
|
101
|
+
# as a hash, except it allows for duplicate keys to be
|
102
|
+
# specified.
|
103
|
+
# * String - taken literally
|
104
|
+
# * Symbol - taken as a boolean column argument (e.g. WHERE active)
|
105
|
+
# * Sequel::SQL::BooleanExpression - an existing condition expression,
|
106
|
+
# probably created using the Sequel expression filter DSL.
|
107
|
+
#
|
108
|
+
# filter also takes a block, which should return one of the above argument
|
109
|
+
# types, and is treated the same way. This block yields a virtual row object,
|
110
|
+
# which is easy to use to create identifiers and functions. For more details
|
111
|
+
# on the virtual row support, see the {"Virtual Rows" guide}[link:files/doc/virtual_rows_rdoc.html]
|
112
|
+
#
|
113
|
+
# If both a block and regular argument
|
114
|
+
# are provided, they get ANDed together.
|
115
|
+
#
|
116
|
+
# Examples:
|
117
|
+
#
|
118
|
+
# dataset.filter(:id => 3).sql #=>
|
119
|
+
# "SELECT * FROM items WHERE (id = 3)"
|
120
|
+
# dataset.filter('price < ?', 100).sql #=>
|
121
|
+
# "SELECT * FROM items WHERE price < 100"
|
122
|
+
# dataset.filter([[:id, (1,2,3)], [:id, 0..10]]).sql #=>
|
123
|
+
# "SELECT * FROM items WHERE ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10)))"
|
124
|
+
# dataset.filter('price < 100').sql #=>
|
125
|
+
# "SELECT * FROM items WHERE price < 100"
|
126
|
+
# dataset.filter(:active).sql #=>
|
127
|
+
# "SELECT * FROM items WHERE :active
|
128
|
+
# dataset.filter{|o| o.price < 100}.sql #=>
|
129
|
+
# "SELECT * FROM items WHERE (price < 100)"
|
130
|
+
#
|
131
|
+
# Multiple filter calls can be chained for scoping:
|
132
|
+
#
|
133
|
+
# software = dataset.filter(:category => 'software')
|
134
|
+
# software.filter{|o| o.price < 100}.sql #=>
|
135
|
+
# "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
|
136
|
+
#
|
137
|
+
# See the the {"Dataset Filtering" guide}[link:files/doc/dataset_filtering_rdoc.html] for more examples and details.
|
138
|
+
def filter(*cond, &block)
|
139
|
+
_filter(@opts[:having] ? :having : :where, *cond, &block)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns a cloned dataset with a :update lock style.
|
143
|
+
def for_update
|
144
|
+
lock_style(:update)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns a copy of the dataset with the source changed.
|
148
|
+
#
|
149
|
+
# dataset.from # SQL: SELECT *
|
150
|
+
# dataset.from(:blah) # SQL: SELECT * FROM blah
|
151
|
+
# dataset.from(:blah, :foo) # SQL: SELECT * FROM blah, foo
|
152
|
+
def from(*source)
|
153
|
+
table_alias_num = 0
|
154
|
+
sources = []
|
155
|
+
source.each do |s|
|
156
|
+
case s
|
157
|
+
when Hash
|
158
|
+
s.each{|k,v| sources << SQL::AliasedExpression.new(k,v)}
|
159
|
+
when Dataset
|
160
|
+
sources << SQL::AliasedExpression.new(s, dataset_alias(table_alias_num+=1))
|
161
|
+
when Symbol
|
162
|
+
sch, table, aliaz = split_symbol(s)
|
163
|
+
if aliaz
|
164
|
+
s = sch ? SQL::QualifiedIdentifier.new(sch.to_sym, table.to_sym) : SQL::Identifier.new(table.to_sym)
|
165
|
+
sources << SQL::AliasedExpression.new(s, aliaz.to_sym)
|
166
|
+
else
|
167
|
+
sources << s
|
168
|
+
end
|
169
|
+
else
|
170
|
+
sources << s
|
171
|
+
end
|
172
|
+
end
|
173
|
+
o = {:from=>sources.empty? ? nil : sources}
|
174
|
+
o[:num_dataset_sources] = table_alias_num if table_alias_num > 0
|
175
|
+
clone(o)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns a dataset selecting from the current dataset.
|
179
|
+
# Supplying the :alias option controls the name of the result.
|
180
|
+
#
|
181
|
+
# ds = DB[:items].order(:name).select(:id, :name)
|
182
|
+
# ds.sql #=> "SELECT id,name FROM items ORDER BY name"
|
183
|
+
# ds.from_self.sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS t1"
|
184
|
+
# ds.from_self(:alias=>:foo).sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS foo"
|
185
|
+
def from_self(opts={})
|
186
|
+
fs = {}
|
187
|
+
@opts.keys.each{|k| fs[k] = nil unless NON_SQL_OPTIONS.include?(k)}
|
188
|
+
clone(fs).from(opts[:alias] ? as(opts[:alias]) : self)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Pattern match any of the columns to any of the terms. The terms can be
|
192
|
+
# strings (which use LIKE) or regular expressions (which are only supported
|
193
|
+
# in some databases). See Sequel::SQL::StringExpression.like. Note that the
|
194
|
+
# total number of pattern matches will be cols.length * terms.length,
|
195
|
+
# which could cause performance issues.
|
196
|
+
#
|
197
|
+
# dataset.grep(:a, '%test%') # SQL: SELECT * FROM items WHERE a LIKE '%test%'
|
198
|
+
# 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'
|
199
|
+
def grep(cols, terms)
|
200
|
+
filter(SQL::BooleanExpression.new(:OR, *Array(cols).collect{|c| SQL::StringExpression.like(c, *terms)}))
|
201
|
+
end
|
202
|
+
|
203
|
+
# Returns a copy of the dataset with the results grouped by the value of
|
204
|
+
# the given columns.
|
205
|
+
#
|
206
|
+
# dataset.group(:id) # SELECT * FROM items GROUP BY id
|
207
|
+
# dataset.group(:id, :name) # SELECT * FROM items GROUP BY id, name
|
208
|
+
def group(*columns)
|
209
|
+
clone(:group => (columns.compact.empty? ? nil : columns))
|
210
|
+
end
|
211
|
+
alias group_by group
|
212
|
+
|
213
|
+
# Returns a dataset grouped by the given column with count by group,
|
214
|
+
# order by the count of records. Column aliases may be supplied, and will
|
215
|
+
# be included in the select clause.
|
216
|
+
#
|
217
|
+
# Examples:
|
218
|
+
#
|
219
|
+
# ds.group_and_count(:name).all => [{:name=>'a', :count=>1}, ...]
|
220
|
+
# ds.group_and_count(:first_name, :last_name).all => [{:first_name=>'a', :last_name=>'b', :count=>1}, ...]
|
221
|
+
# ds.group_and_count(:first_name___name).all => [{:name=>'a', :count=>1}, ...]
|
222
|
+
def group_and_count(*columns)
|
223
|
+
group(*columns.map{|c| unaliased_identifier(c)}).select(*(columns + [COUNT_OF_ALL_AS_COUNT]))
|
224
|
+
end
|
225
|
+
|
226
|
+
# Returns a copy of the dataset with the HAVING conditions changed. See #filter for argument types.
|
227
|
+
#
|
228
|
+
# dataset.group(:sum).having(:sum=>10) # SQL: SELECT * FROM items GROUP BY sum HAVING sum = 10
|
229
|
+
def having(*cond, &block)
|
230
|
+
_filter(:having, *cond, &block)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Adds an INTERSECT clause using a second dataset object.
|
234
|
+
# An INTERSECT compound dataset returns all rows in both the current dataset
|
235
|
+
# and the given dataset.
|
236
|
+
# Raises an InvalidOperation if the operation is not supported.
|
237
|
+
# Options:
|
238
|
+
# * :all - Set to true to use INTERSECT ALL instead of INTERSECT, so duplicate rows can occur
|
239
|
+
# * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
|
240
|
+
#
|
241
|
+
# DB[:items].intersect(DB[:other_items]).sql
|
242
|
+
# #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
|
243
|
+
def intersect(dataset, opts={})
|
244
|
+
opts = {:all=>opts} unless opts.is_a?(Hash)
|
245
|
+
raise(InvalidOperation, "INTERSECT not supported") unless supports_intersect_except?
|
246
|
+
raise(InvalidOperation, "INTERSECT ALL not supported") if opts[:all] && !supports_intersect_except_all?
|
247
|
+
compound_clone(:intersect, dataset, opts)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Inverts the current filter
|
251
|
+
#
|
252
|
+
# dataset.filter(:category => 'software').invert.sql #=>
|
253
|
+
# "SELECT * FROM items WHERE (category != 'software')"
|
254
|
+
def invert
|
255
|
+
having, where = @opts[:having], @opts[:where]
|
256
|
+
raise(Error, "No current filter") unless having || where
|
257
|
+
o = {}
|
258
|
+
o[:having] = SQL::BooleanExpression.invert(having) if having
|
259
|
+
o[:where] = SQL::BooleanExpression.invert(where) if where
|
260
|
+
clone(o)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Returns a joined dataset. Uses the following arguments:
|
264
|
+
#
|
265
|
+
# * type - The type of join to do (e.g. :inner)
|
266
|
+
# * table - Depends on type:
|
267
|
+
# * Dataset - a subselect is performed with an alias of tN for some value of N
|
268
|
+
# * Model (or anything responding to :table_name) - table.table_name
|
269
|
+
# * String, Symbol: table
|
270
|
+
# * expr - specifies conditions, depends on type:
|
271
|
+
# * Hash, Array with all two pairs - Assumes key (1st arg) is column of joined table (unless already
|
272
|
+
# qualified), and value (2nd arg) is column of the last joined or primary table (or the
|
273
|
+
# :implicit_qualifier option).
|
274
|
+
# To specify multiple conditions on a single joined table column, you must use an array.
|
275
|
+
# Uses a JOIN with an ON clause.
|
276
|
+
# * Array - If all members of the array are symbols, considers them as columns and
|
277
|
+
# uses a JOIN with a USING clause. Most databases will remove duplicate columns from
|
278
|
+
# the result set if this is used.
|
279
|
+
# * nil - If a block is not given, doesn't use ON or USING, so the JOIN should be a NATURAL
|
280
|
+
# or CROSS join. If a block is given, uses a ON clause based on the block, see below.
|
281
|
+
# * Everything else - pretty much the same as a using the argument in a call to filter,
|
282
|
+
# so strings are considered literal, symbols specify boolean columns, and blockless
|
283
|
+
# filter expressions can be used. Uses a JOIN with an ON clause.
|
284
|
+
# * options - a hash of options, with any of the following keys:
|
285
|
+
# * :table_alias - the name of the table's alias when joining, necessary for joining
|
286
|
+
# to the same table more than once. No alias is used by default.
|
287
|
+
# * :implicit_qualifier - The name to use for qualifying implicit conditions. By default,
|
288
|
+
# the last joined or primary table is used.
|
289
|
+
# * block - The block argument should only be given if a JOIN with an ON clause is used,
|
290
|
+
# in which case it yields the table alias/name for the table currently being joined,
|
291
|
+
# the table alias/name for the last joined (or first table), and an array of previous
|
292
|
+
# SQL::JoinClause.
|
293
|
+
def join_table(type, table, expr=nil, options={}, &block)
|
294
|
+
using_join = expr.is_a?(Array) && !expr.empty? && expr.all?{|x| x.is_a?(Symbol)}
|
295
|
+
if using_join && !supports_join_using?
|
296
|
+
h = {}
|
297
|
+
expr.each{|s| h[s] = s}
|
298
|
+
return join_table(type, table, h, options)
|
299
|
+
end
|
300
|
+
|
301
|
+
case options
|
302
|
+
when Hash
|
303
|
+
table_alias = options[:table_alias]
|
304
|
+
last_alias = options[:implicit_qualifier]
|
305
|
+
when Symbol, String, SQL::Identifier
|
306
|
+
table_alias = options
|
307
|
+
last_alias = nil
|
308
|
+
else
|
309
|
+
raise Error, "invalid options format for join_table: #{options.inspect}"
|
310
|
+
end
|
311
|
+
|
312
|
+
if Dataset === table
|
313
|
+
if table_alias.nil?
|
314
|
+
table_alias_num = (@opts[:num_dataset_sources] || 0) + 1
|
315
|
+
table_alias = dataset_alias(table_alias_num)
|
316
|
+
end
|
317
|
+
table_name = table_alias
|
318
|
+
else
|
319
|
+
table = table.table_name if table.respond_to?(:table_name)
|
320
|
+
table_name = table_alias || table
|
321
|
+
end
|
322
|
+
|
323
|
+
join = if expr.nil? and !block_given?
|
324
|
+
SQL::JoinClause.new(type, table, table_alias)
|
325
|
+
elsif using_join
|
326
|
+
raise(Sequel::Error, "can't use a block if providing an array of symbols as expr") if block_given?
|
327
|
+
SQL::JoinUsingClause.new(expr, type, table, table_alias)
|
328
|
+
else
|
329
|
+
last_alias ||= @opts[:last_joined_table] || first_source_alias
|
330
|
+
if Sequel.condition_specifier?(expr)
|
331
|
+
expr = expr.collect do |k, v|
|
332
|
+
k = qualified_column_name(k, table_name) if k.is_a?(Symbol)
|
333
|
+
v = qualified_column_name(v, last_alias) if v.is_a?(Symbol)
|
334
|
+
[k,v]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
if block_given?
|
338
|
+
expr2 = yield(table_name, last_alias, @opts[:join] || [])
|
339
|
+
expr = expr ? SQL::BooleanExpression.new(:AND, expr, expr2) : expr2
|
340
|
+
end
|
341
|
+
SQL::JoinOnClause.new(expr, type, table, table_alias)
|
342
|
+
end
|
343
|
+
|
344
|
+
opts = {:join => (@opts[:join] || []) + [join], :last_joined_table => table_name}
|
345
|
+
opts[:num_dataset_sources] = table_alias_num if table_alias_num
|
346
|
+
clone(opts)
|
347
|
+
end
|
348
|
+
|
349
|
+
CONDITIONED_JOIN_TYPES.each do |jtype|
|
350
|
+
class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end", __FILE__, __LINE__)
|
351
|
+
end
|
352
|
+
UNCONDITIONED_JOIN_TYPES.each do |jtype|
|
353
|
+
class_eval("def #{jtype}_join(table); raise(Sequel::Error, '#{jtype}_join does not accept join table blocks') if block_given?; join_table(:#{jtype}, table) end", __FILE__, __LINE__)
|
354
|
+
end
|
355
|
+
alias join inner_join
|
356
|
+
|
357
|
+
# If given an integer, the dataset will contain only the first l results.
|
358
|
+
# If given a range, it will contain only those at offsets within that
|
359
|
+
# range. If a second argument is given, it is used as an offset.
|
360
|
+
#
|
361
|
+
# dataset.limit(10) # SQL: SELECT * FROM items LIMIT 10
|
362
|
+
# dataset.limit(10, 20) # SQL: SELECT * FROM items LIMIT 10 OFFSET 20
|
363
|
+
def limit(l, o = nil)
|
364
|
+
return from_self.limit(l, o) if @opts[:sql]
|
365
|
+
|
366
|
+
if Range === l
|
367
|
+
o = l.first
|
368
|
+
l = l.last - l.first + (l.exclude_end? ? 0 : 1)
|
369
|
+
end
|
370
|
+
l = l.to_i if l.is_a?(String) && !l.is_a?(LiteralString)
|
371
|
+
if l.is_a?(Integer)
|
372
|
+
raise(Error, 'Limits must be greater than or equal to 1') unless l >= 1
|
373
|
+
end
|
374
|
+
opts = {:limit => l}
|
375
|
+
if o
|
376
|
+
o = o.to_i if o.is_a?(String) && !o.is_a?(LiteralString)
|
377
|
+
if o.is_a?(Integer)
|
378
|
+
raise(Error, 'Offsets must be greater than or equal to 0') unless o >= 0
|
379
|
+
end
|
380
|
+
opts[:offset] = o
|
381
|
+
end
|
382
|
+
clone(opts)
|
383
|
+
end
|
384
|
+
|
385
|
+
# Returns a cloned dataset with the given lock style. If style is a
|
386
|
+
# string, it will be used directly. Otherwise, a symbol may be used
|
387
|
+
# for database independent locking. Currently :update is respected
|
388
|
+
# by most databases, and :share is supported by some.
|
389
|
+
def lock_style(style)
|
390
|
+
clone(:lock => style)
|
391
|
+
end
|
392
|
+
|
393
|
+
# Returns a naked dataset clone - i.e. a dataset that returns records as
|
394
|
+
# hashes instead of calling the row proc.
|
395
|
+
def naked
|
396
|
+
ds = clone
|
397
|
+
ds.row_proc = nil
|
398
|
+
ds
|
399
|
+
end
|
400
|
+
|
401
|
+
# Adds an alternate filter to an existing filter using OR. If no filter
|
402
|
+
# exists an error is raised.
|
403
|
+
#
|
404
|
+
# dataset.filter(:a).or(:b) # SQL: SELECT * FROM items WHERE a OR b
|
405
|
+
def or(*cond, &block)
|
406
|
+
clause = (@opts[:having] ? :having : :where)
|
407
|
+
raise(InvalidOperation, "No existing filter found.") unless @opts[clause]
|
408
|
+
cond = cond.first if cond.size == 1
|
409
|
+
clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(cond, &block)))
|
410
|
+
end
|
411
|
+
|
412
|
+
# Returns a copy of the dataset with the order changed. If a nil is given
|
413
|
+
# the returned dataset has no order. This can accept multiple arguments
|
414
|
+
# of varying kinds, and even SQL functions. If a block is given, it is treated
|
415
|
+
# as a virtual row block, similar to filter.
|
416
|
+
#
|
417
|
+
# ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
|
418
|
+
# ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
|
419
|
+
# ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
|
420
|
+
# ds.order(:a + :b).sql #=> 'SELECT * FROM items ORDER BY (a + b)'
|
421
|
+
# ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
|
422
|
+
# ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
|
423
|
+
# ds.order{|o| o.sum(:name)}.sql #=> 'SELECT * FROM items ORDER BY sum(name)'
|
424
|
+
# ds.order(nil).sql #=> 'SELECT * FROM items'
|
425
|
+
def order(*columns, &block)
|
426
|
+
columns += Array(Sequel.virtual_row(&block)) if block
|
427
|
+
clone(:order => (columns.compact.empty?) ? nil : columns)
|
428
|
+
end
|
429
|
+
alias order_by order
|
430
|
+
|
431
|
+
# Returns a copy of the dataset with the order columns added
|
432
|
+
# to the existing order.
|
433
|
+
#
|
434
|
+
# ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
|
435
|
+
# ds.order(:a).order_more(:b).sql #=> 'SELECT * FROM items ORDER BY a, b'
|
436
|
+
def order_more(*columns, &block)
|
437
|
+
columns = @opts[:order] + columns if @opts[:order]
|
438
|
+
order(*columns, &block)
|
439
|
+
end
|
440
|
+
|
441
|
+
# Qualify to the given table, or first source if not table is given.
|
442
|
+
def qualify(table=first_source)
|
443
|
+
qualify_to(table)
|
444
|
+
end
|
445
|
+
|
446
|
+
# Return a copy of the dataset with unqualified identifiers in the
|
447
|
+
# SELECT, WHERE, GROUP, HAVING, and ORDER clauses qualified by the
|
448
|
+
# given table. If no columns are currently selected, select all
|
449
|
+
# columns of the given table.
|
450
|
+
def qualify_to(table)
|
451
|
+
o = @opts
|
452
|
+
return clone if o[:sql]
|
453
|
+
h = {}
|
454
|
+
(o.keys & QUALIFY_KEYS).each do |k|
|
455
|
+
h[k] = qualified_expression(o[k], table)
|
456
|
+
end
|
457
|
+
h[:select] = [SQL::ColumnAll.new(table)] if !o[:select] || o[:select].empty?
|
458
|
+
clone(h)
|
459
|
+
end
|
460
|
+
|
461
|
+
# Qualify the dataset to its current first source. This is useful
|
462
|
+
# if you have unqualified identifiers in the query that all refer to
|
463
|
+
# the first source, and you want to join to another table which
|
464
|
+
# has columns with the same name as columns in the current dataset.
|
465
|
+
# See qualify_to.
|
466
|
+
def qualify_to_first_source
|
467
|
+
qualify_to(first_source)
|
468
|
+
end
|
469
|
+
|
470
|
+
# Returns a copy of the dataset with the order reversed. If no order is
|
471
|
+
# given, the existing order is inverted.
|
472
|
+
def reverse_order(*order)
|
473
|
+
order(*invert_order(order.empty? ? @opts[:order] : order))
|
474
|
+
end
|
475
|
+
alias reverse reverse_order
|
476
|
+
|
477
|
+
# Returns a copy of the dataset with the columns selected changed
|
478
|
+
# to the given columns. This also takes a virtual row block,
|
479
|
+
# similar to filter.
|
480
|
+
#
|
481
|
+
# dataset.select(:a) # SELECT a FROM items
|
482
|
+
# dataset.select(:a, :b) # SELECT a, b FROM items
|
483
|
+
# dataset.select{|o| [o.a, o.sum(:b)]} # SELECT a, sum(b) FROM items
|
484
|
+
def select(*columns, &block)
|
485
|
+
columns += Array(Sequel.virtual_row(&block)) if block
|
486
|
+
m = []
|
487
|
+
columns.map do |i|
|
488
|
+
i.is_a?(Hash) ? m.concat(i.map{|k, v| SQL::AliasedExpression.new(k,v)}) : m << i
|
489
|
+
end
|
490
|
+
clone(:select => m)
|
491
|
+
end
|
492
|
+
|
493
|
+
# Returns a copy of the dataset selecting the wildcard.
|
494
|
+
#
|
495
|
+
# dataset.select(:a).select_all # SELECT * FROM items
|
496
|
+
def select_all
|
497
|
+
clone(:select => nil)
|
498
|
+
end
|
499
|
+
|
500
|
+
# Returns a copy of the dataset with the given columns added
|
501
|
+
# to the existing selected columns. If no columns are currently selected
|
502
|
+
# it will select the columns given in addition to *.
|
503
|
+
#
|
504
|
+
# dataset.select(:a).select(:b) # SELECT b FROM items
|
505
|
+
# dataset.select(:a).select_append(:b) # SELECT a, b FROM items
|
506
|
+
# dataset.select_append(:b) # SELECT *, b FROM items
|
507
|
+
def select_append(*columns, &block)
|
508
|
+
cur_sel = @opts[:select]
|
509
|
+
cur_sel = [WILDCARD] if !cur_sel || cur_sel.empty?
|
510
|
+
select(*(cur_sel + columns), &block)
|
511
|
+
end
|
512
|
+
|
513
|
+
# Returns a copy of the dataset with the given columns added
|
514
|
+
# to the existing selected columns. If no columns are currently selected
|
515
|
+
# it will just select the columns given.
|
516
|
+
#
|
517
|
+
# dataset.select(:a).select(:b) # SELECT b FROM items
|
518
|
+
# dataset.select(:a).select_more(:b) # SELECT a, b FROM items
|
519
|
+
# dataset.select_more(:b) # SELECT b FROM items
|
520
|
+
def select_more(*columns, &block)
|
521
|
+
columns = @opts[:select] + columns if @opts[:select]
|
522
|
+
select(*columns, &block)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Set the server for this dataset to use. Used to pick a specific database
|
526
|
+
# shard to run a query against, or to override the default (which is SELECT uses
|
527
|
+
# :read_only database and all other queries use the :default database).
|
528
|
+
def server(servr)
|
529
|
+
clone(:server=>servr)
|
530
|
+
end
|
531
|
+
|
532
|
+
# Set the default values for insert and update statements. The values hash passed
|
533
|
+
# to insert or update are merged into this hash.
|
534
|
+
def set_defaults(hash)
|
535
|
+
clone(:defaults=>(@opts[:defaults]||{}).merge(hash))
|
536
|
+
end
|
537
|
+
|
538
|
+
# Set values that override hash arguments given to insert and update statements.
|
539
|
+
# This hash is merged into the hash provided to insert or update.
|
540
|
+
def set_overrides(hash)
|
541
|
+
clone(:overrides=>hash.merge(@opts[:overrides]||{}))
|
542
|
+
end
|
543
|
+
|
544
|
+
# Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
|
545
|
+
#
|
546
|
+
# dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
|
547
|
+
def unfiltered
|
548
|
+
clone(:where => nil, :having => nil)
|
549
|
+
end
|
550
|
+
|
551
|
+
# Returns a copy of the dataset with no grouping (GROUP or HAVING clause) applied.
|
552
|
+
#
|
553
|
+
# dataset.group(:a).having(:a=>1).where(:b).ungrouped # SELECT * FROM items WHERE b
|
554
|
+
def ungrouped
|
555
|
+
clone(:group => nil, :having => nil)
|
556
|
+
end
|
557
|
+
|
558
|
+
# Adds a UNION clause using a second dataset object.
|
559
|
+
# A UNION compound dataset returns all rows in either the current dataset
|
560
|
+
# or the given dataset.
|
561
|
+
# Options:
|
562
|
+
# * :all - Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
|
563
|
+
# * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
|
564
|
+
#
|
565
|
+
# DB[:items].union(DB[:other_items]).sql
|
566
|
+
# #=> "SELECT * FROM items UNION SELECT * FROM other_items"
|
567
|
+
def union(dataset, opts={})
|
568
|
+
opts = {:all=>opts} unless opts.is_a?(Hash)
|
569
|
+
compound_clone(:union, dataset, opts)
|
570
|
+
end
|
571
|
+
|
572
|
+
# Returns a copy of the dataset with no limit or offset.
|
573
|
+
#
|
574
|
+
# dataset.limit(10, 20).unlimited # SELECT * FROM items
|
575
|
+
def unlimited
|
576
|
+
clone(:limit=>nil, :offset=>nil)
|
577
|
+
end
|
578
|
+
|
579
|
+
# Returns a copy of the dataset with no order.
|
580
|
+
#
|
581
|
+
# dataset.order(:a).unordered # SELECT * FROM items
|
582
|
+
def unordered
|
583
|
+
order(nil)
|
584
|
+
end
|
585
|
+
|
586
|
+
# Add a condition to the WHERE clause. See #filter for argument types.
|
587
|
+
#
|
588
|
+
# dataset.group(:a).having(:a).filter(:b) # SELECT * FROM items GROUP BY a HAVING a AND b
|
589
|
+
# dataset.group(:a).having(:a).where(:b) # SELECT * FROM items WHERE b GROUP BY a HAVING a
|
590
|
+
def where(*cond, &block)
|
591
|
+
_filter(:where, *cond, &block)
|
592
|
+
end
|
593
|
+
|
594
|
+
# Add a simple common table expression (CTE) with the given name and a dataset that defines the CTE.
|
595
|
+
# A common table expression acts as an inline view for the query.
|
596
|
+
# Options:
|
597
|
+
# * :args - Specify the arguments/columns for the CTE, should be an array of symbols.
|
598
|
+
# * :recursive - Specify that this is a recursive CTE
|
599
|
+
def with(name, dataset, opts={})
|
600
|
+
raise(Error, 'This datatset does not support common table expressions') unless supports_cte?
|
601
|
+
clone(:with=>(@opts[:with]||[]) + [opts.merge(:name=>name, :dataset=>dataset)])
|
602
|
+
end
|
603
|
+
|
604
|
+
# Add a recursive common table expression (CTE) with the given name, a dataset that
|
605
|
+
# defines the nonrecursive part of the CTE, and a dataset that defines the recursive part
|
606
|
+
# of the CTE. Options:
|
607
|
+
# * :args - Specify the arguments/columns for the CTE, should be an array of symbols.
|
608
|
+
# * :union_all - Set to false to use UNION instead of UNION ALL combining the nonrecursive and recursive parts.
|
609
|
+
def with_recursive(name, nonrecursive, recursive, opts={})
|
610
|
+
raise(Error, 'This datatset does not support common table expressions') unless supports_cte?
|
611
|
+
clone(:with=>(@opts[:with]||[]) + [opts.merge(:recursive=>true, :name=>name, :dataset=>nonrecursive.union(recursive, {:all=>opts[:union_all] != false, :from_self=>false}))])
|
612
|
+
end
|
613
|
+
|
614
|
+
# Returns a copy of the dataset with the static SQL used. This is useful if you want
|
615
|
+
# to keep the same row_proc/graph, but change the SQL used to custom SQL.
|
616
|
+
#
|
617
|
+
# dataset.with_sql('SELECT * FROM foo') # SELECT * FROM foo
|
618
|
+
def with_sql(sql, *args)
|
619
|
+
sql = SQL::PlaceholderLiteralString.new(sql, args) unless args.empty?
|
620
|
+
clone(:sql=>sql)
|
621
|
+
end
|
622
|
+
|
623
|
+
protected
|
624
|
+
|
625
|
+
# Return true if the dataset has a non-nil value for any key in opts.
|
626
|
+
def options_overlap(opts)
|
627
|
+
!(@opts.collect{|k,v| k unless v.nil?}.compact & opts).empty?
|
628
|
+
end
|
629
|
+
|
630
|
+
# Whether this dataset is a simple SELECT * FROM table.
|
631
|
+
def simple_select_all?
|
632
|
+
o = @opts.reject{|k,v| v.nil? || NON_SQL_OPTIONS.include?(k)}
|
633
|
+
o.length == 1 && (f = o[:from]) && f.length == 1 && f.first.is_a?(Symbol)
|
634
|
+
end
|
635
|
+
|
636
|
+
private
|
637
|
+
|
638
|
+
# Internal filter method so it works on either the having or where clauses.
|
639
|
+
def _filter(clause, *cond, &block)
|
640
|
+
cond = cond.first if cond.size == 1
|
641
|
+
if cond.respond_to?(:empty?) && cond.empty? && !block
|
642
|
+
clone
|
643
|
+
else
|
644
|
+
cond = filter_expr(cond, &block)
|
645
|
+
cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
|
646
|
+
clone(clause => cond)
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
# Add the dataset to the list of compounds
|
651
|
+
def compound_clone(type, dataset, opts)
|
652
|
+
ds = compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, opts[:all]]])
|
653
|
+
opts[:from_self] == false ? ds : ds.from_self(opts)
|
654
|
+
end
|
655
|
+
|
656
|
+
# SQL fragment based on the expr type. See #filter.
|
657
|
+
def filter_expr(expr = nil, &block)
|
658
|
+
expr = nil if expr == []
|
659
|
+
if expr && block
|
660
|
+
return SQL::BooleanExpression.new(:AND, filter_expr(expr), filter_expr(block))
|
661
|
+
elsif block
|
662
|
+
expr = block
|
663
|
+
end
|
664
|
+
case expr
|
665
|
+
when Hash
|
666
|
+
SQL::BooleanExpression.from_value_pairs(expr)
|
667
|
+
when Array
|
668
|
+
if (sexpr = expr.at(0)).is_a?(String)
|
669
|
+
SQL::PlaceholderLiteralString.new(sexpr, expr[1..-1], true)
|
670
|
+
elsif Sequel.condition_specifier?(expr)
|
671
|
+
SQL::BooleanExpression.from_value_pairs(expr)
|
672
|
+
else
|
673
|
+
SQL::BooleanExpression.new(:AND, *expr.map{|x| filter_expr(x)})
|
674
|
+
end
|
675
|
+
when Proc
|
676
|
+
filter_expr(Sequel.virtual_row(&expr))
|
677
|
+
when SQL::NumericExpression, SQL::StringExpression
|
678
|
+
raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
|
679
|
+
when Symbol, SQL::Expression
|
680
|
+
expr
|
681
|
+
when TrueClass, FalseClass
|
682
|
+
SQL::BooleanExpression.new(:NOOP, expr)
|
683
|
+
when String
|
684
|
+
LiteralString.new("(#{expr})")
|
685
|
+
else
|
686
|
+
raise(Error, 'Invalid filter argument')
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
# Inverts the given order by breaking it into a list of column references
|
691
|
+
# and inverting them.
|
692
|
+
#
|
693
|
+
# dataset.invert_order([:id.desc]]) #=> [:id]
|
694
|
+
# dataset.invert_order(:category, :price.desc]) #=>
|
695
|
+
# [:category.desc, :price]
|
696
|
+
def invert_order(order)
|
697
|
+
return nil unless order
|
698
|
+
new_order = []
|
699
|
+
order.map do |f|
|
700
|
+
case f
|
701
|
+
when SQL::OrderedExpression
|
702
|
+
f.invert
|
703
|
+
else
|
704
|
+
SQL::OrderedExpression.new(f)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|
709
|
+
end
|