sequel 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. data/CHANGELOG +68 -0
  2. data/COPYING +1 -1
  3. data/README.rdoc +87 -27
  4. data/bin/sequel +2 -4
  5. data/doc/association_basics.rdoc +1383 -0
  6. data/doc/dataset_basics.rdoc +106 -0
  7. data/doc/opening_databases.rdoc +45 -16
  8. data/doc/querying.rdoc +210 -0
  9. data/doc/release_notes/3.11.0.txt +254 -0
  10. data/doc/virtual_rows.rdoc +217 -31
  11. data/lib/sequel/adapters/ado.rb +28 -12
  12. data/lib/sequel/adapters/ado/mssql.rb +33 -1
  13. data/lib/sequel/adapters/amalgalite.rb +13 -8
  14. data/lib/sequel/adapters/db2.rb +1 -2
  15. data/lib/sequel/adapters/dbi.rb +7 -4
  16. data/lib/sequel/adapters/do.rb +14 -15
  17. data/lib/sequel/adapters/do/postgres.rb +4 -5
  18. data/lib/sequel/adapters/do/sqlite.rb +9 -0
  19. data/lib/sequel/adapters/firebird.rb +5 -10
  20. data/lib/sequel/adapters/informix.rb +2 -4
  21. data/lib/sequel/adapters/jdbc.rb +111 -49
  22. data/lib/sequel/adapters/jdbc/mssql.rb +1 -2
  23. data/lib/sequel/adapters/jdbc/mysql.rb +11 -0
  24. data/lib/sequel/adapters/jdbc/oracle.rb +4 -7
  25. data/lib/sequel/adapters/jdbc/postgresql.rb +8 -1
  26. data/lib/sequel/adapters/jdbc/sqlite.rb +12 -0
  27. data/lib/sequel/adapters/mysql.rb +14 -5
  28. data/lib/sequel/adapters/odbc.rb +2 -4
  29. data/lib/sequel/adapters/odbc/mssql.rb +2 -4
  30. data/lib/sequel/adapters/openbase.rb +1 -2
  31. data/lib/sequel/adapters/oracle.rb +4 -8
  32. data/lib/sequel/adapters/postgres.rb +4 -11
  33. data/lib/sequel/adapters/shared/mssql.rb +22 -9
  34. data/lib/sequel/adapters/shared/mysql.rb +33 -30
  35. data/lib/sequel/adapters/shared/oracle.rb +0 -5
  36. data/lib/sequel/adapters/shared/postgres.rb +13 -11
  37. data/lib/sequel/adapters/shared/sqlite.rb +56 -10
  38. data/lib/sequel/adapters/sqlite.rb +16 -9
  39. data/lib/sequel/connection_pool.rb +6 -1
  40. data/lib/sequel/connection_pool/single.rb +1 -0
  41. data/lib/sequel/core.rb +6 -1
  42. data/lib/sequel/database.rb +52 -23
  43. data/lib/sequel/database/schema_generator.rb +6 -0
  44. data/lib/sequel/database/schema_methods.rb +5 -5
  45. data/lib/sequel/database/schema_sql.rb +1 -1
  46. data/lib/sequel/dataset.rb +4 -190
  47. data/lib/sequel/dataset/actions.rb +323 -1
  48. data/lib/sequel/dataset/features.rb +18 -2
  49. data/lib/sequel/dataset/graph.rb +7 -0
  50. data/lib/sequel/dataset/misc.rb +119 -0
  51. data/lib/sequel/dataset/mutation.rb +64 -0
  52. data/lib/sequel/dataset/prepared_statements.rb +6 -0
  53. data/lib/sequel/dataset/query.rb +272 -6
  54. data/lib/sequel/dataset/sql.rb +186 -394
  55. data/lib/sequel/model.rb +4 -2
  56. data/lib/sequel/model/associations.rb +31 -14
  57. data/lib/sequel/model/base.rb +32 -13
  58. data/lib/sequel/model/exceptions.rb +8 -4
  59. data/lib/sequel/model/plugins.rb +3 -13
  60. data/lib/sequel/plugins/active_model.rb +26 -7
  61. data/lib/sequel/plugins/instance_filters.rb +98 -0
  62. data/lib/sequel/plugins/many_through_many.rb +1 -1
  63. data/lib/sequel/plugins/optimistic_locking.rb +25 -9
  64. data/lib/sequel/version.rb +1 -1
  65. data/spec/adapters/mssql_spec.rb +26 -0
  66. data/spec/adapters/mysql_spec.rb +33 -4
  67. data/spec/adapters/postgres_spec.rb +24 -1
  68. data/spec/adapters/spec_helper.rb +6 -0
  69. data/spec/adapters/sqlite_spec.rb +28 -0
  70. data/spec/core/connection_pool_spec.rb +17 -5
  71. data/spec/core/database_spec.rb +101 -1
  72. data/spec/core/dataset_spec.rb +42 -4
  73. data/spec/core/schema_spec.rb +13 -0
  74. data/spec/extensions/active_model_spec.rb +34 -11
  75. data/spec/extensions/caching_spec.rb +2 -0
  76. data/spec/extensions/instance_filters_spec.rb +55 -0
  77. data/spec/extensions/spec_helper.rb +2 -0
  78. data/spec/integration/dataset_test.rb +12 -1
  79. data/spec/integration/model_test.rb +12 -0
  80. data/spec/integration/plugin_test.rb +61 -1
  81. data/spec/integration/schema_test.rb +14 -3
  82. data/spec/model/base_spec.rb +27 -0
  83. data/spec/model/plugins_spec.rb +0 -22
  84. data/spec/model/record_spec.rb +32 -1
  85. data/spec/model/spec_helper.rb +2 -0
  86. metadata +14 -3
  87. data/lib/sequel/dataset/convenience.rb +0 -326
@@ -1,10 +1,26 @@
1
1
  module Sequel
2
2
  class Dataset
3
+ # ---------------------
4
+ # :section: Methods that describe what the dataset supports
5
+ # These methods all return booleans, with most describing whether or not the
6
+ # dataset supports a feature.
7
+ # ---------------------
8
+
9
+ # Method used to check if WITH is supported
10
+ WITH_SUPPORTED=:select_with_sql
11
+
3
12
  # Whether this dataset quotes identifiers.
4
13
  def quote_identifiers?
5
14
  @quote_identifiers
6
15
  end
7
16
 
17
+ # Whether this dataset will provide accurate number of rows matched for
18
+ # delete and update statements. Accurate in this case is the number of
19
+ # rows matched by the dataset's filter.
20
+ def provides_accurate_rows_matched?
21
+ true
22
+ end
23
+
8
24
  # Whether the dataset requires SQL standard datetimes (false by default,
9
25
  # as most allow strings with ISO 8601 format.
10
26
  def requires_sql_standard_datetimes?
@@ -16,9 +32,9 @@ module Sequel
16
32
  select_clause_methods.include?(WITH_SUPPORTED)
17
33
  end
18
34
 
19
- # Whether the dataset supports the DISTINCT ON clause, true by default.
35
+ # Whether the dataset supports the DISTINCT ON clause, false by default.
20
36
  def supports_distinct_on?
21
- true
37
+ false
22
38
  end
23
39
 
24
40
  # Whether the dataset supports the INTERSECT and EXCEPT compound operations, true by default.
@@ -1,5 +1,12 @@
1
1
  module Sequel
2
2
  class Dataset
3
+ # ---------------------
4
+ # :section: Methods related to dataset graphing
5
+ # Dataset graphing changes the dataset to yield hashes where keys are table
6
+ # name symbols and columns are hashes representing the values related to
7
+ # that table. All of these methods return modified copies of the receiver.
8
+ # ---------------------
9
+
3
10
  # Adds the given graph aliases to the list of graph aliases to use,
4
11
  # unlike #set_graph_aliases, which replaces the list. See
5
12
  # #set_graph_aliases.
@@ -0,0 +1,119 @@
1
+ module Sequel
2
+ class Dataset
3
+ # ---------------------
4
+ # :section: Miscellaneous methods
5
+ # These methods don't fit cleanly into another section.
6
+ # ---------------------
7
+
8
+ NOTIMPL_MSG = "This method must be overridden in Sequel adapters".freeze
9
+ ARRAY_ACCESS_ERROR_MSG = 'You cannot call Dataset#[] with an integer or with no arguments.'.freeze
10
+ ARG_BLOCK_ERROR_MSG = 'Must use either an argument or a block, not both'.freeze
11
+ IMPORT_ERROR_MSG = 'Using Sequel::Dataset#import an empty column array is not allowed'.freeze
12
+
13
+ # The database that corresponds to this dataset
14
+ attr_accessor :db
15
+
16
+ # The hash of options for this dataset, keys are symbols.
17
+ attr_accessor :opts
18
+
19
+ # Constructs a new Dataset instance with an associated database and
20
+ # options. Datasets are usually constructed by invoking the Database#[] method:
21
+ #
22
+ # DB[:posts]
23
+ #
24
+ # Sequel::Dataset is an abstract class that is not useful by itself. Each
25
+ # database adaptor should provide a subclass of Sequel::Dataset, and have
26
+ # the Database#dataset method return an instance of that class.
27
+ def initialize(db, opts = nil)
28
+ @db = db
29
+ @quote_identifiers = db.quote_identifiers? if db.respond_to?(:quote_identifiers?)
30
+ @identifier_input_method = db.identifier_input_method if db.respond_to?(:identifier_input_method)
31
+ @identifier_output_method = db.identifier_output_method if db.respond_to?(:identifier_output_method)
32
+ @opts = opts || {}
33
+ @row_proc = nil
34
+ end
35
+
36
+ # Return the dataset as an aliased expression with the given alias. You can
37
+ # use this as a FROM or JOIN dataset, or as a column if this dataset
38
+ # returns a single row and column.
39
+ def as(aliaz)
40
+ ::Sequel::SQL::AliasedExpression.new(self, aliaz)
41
+ end
42
+
43
+ # Yield a dataset for each server in the connection pool that is tied to that server.
44
+ # Intended for use in sharded environments where all servers need to be modified
45
+ # with the same data:
46
+ #
47
+ # DB[:configs].where(:key=>'setting').each_server{|ds| ds.update(:value=>'new_value')}
48
+ def each_server
49
+ db.servers.each{|s| yield server(s)}
50
+ end
51
+
52
+ # The first source (primary table) for this dataset. If the dataset doesn't
53
+ # have a table, raises an error. If the table is aliased, returns the aliased name.
54
+ def first_source_alias
55
+ source = @opts[:from]
56
+ if source.nil? || source.empty?
57
+ raise Error, 'No source specified for query'
58
+ end
59
+ case s = source.first
60
+ when SQL::AliasedExpression
61
+ s.aliaz
62
+ when Symbol
63
+ sch, table, aliaz = split_symbol(s)
64
+ aliaz ? aliaz.to_sym : s
65
+ else
66
+ s
67
+ end
68
+ end
69
+ alias first_source first_source_alias
70
+
71
+ # The first source (primary table) for this dataset. If the dataset doesn't
72
+ # have a table, raises an error. If the table is aliased, returns the original
73
+ # table, not the alias
74
+ def first_source_table
75
+ source = @opts[:from]
76
+ if source.nil? || source.empty?
77
+ raise Error, 'No source specified for query'
78
+ end
79
+ case s = source.first
80
+ when SQL::AliasedExpression
81
+ s.expression
82
+ when Symbol
83
+ sch, table, aliaz = split_symbol(s)
84
+ aliaz ? (sch ? SQL::QualifiedIdentifier.new(sch, table) : table.to_sym) : s
85
+ else
86
+ s
87
+ end
88
+ end
89
+
90
+ # Returns a string representation of the dataset including the class name
91
+ # and the corresponding SQL select statement.
92
+ def inspect
93
+ "#<#{self.class}: #{sql.inspect}>"
94
+ end
95
+
96
+ # Creates a unique table alias that hasn't already been used in the dataset.
97
+ # table_alias can be any type of object accepted by alias_symbol.
98
+ # The symbol returned will be the implicit alias in the argument,
99
+ # possibly appended with "_N" if the implicit alias has already been
100
+ # used, where N is an integer starting at 0 and increasing until an
101
+ # unused one is found.
102
+ def unused_table_alias(table_alias)
103
+ table_alias = alias_symbol(table_alias)
104
+ used_aliases = []
105
+ used_aliases += opts[:from].map{|t| alias_symbol(t)} if opts[:from]
106
+ used_aliases += opts[:join].map{|j| j.table_alias ? alias_alias_symbol(j.table_alias) : alias_symbol(j.table)} if opts[:join]
107
+ if used_aliases.include?(table_alias)
108
+ i = 0
109
+ loop do
110
+ ta = :"#{table_alias}_#{i}"
111
+ return ta unless used_aliases.include?(ta)
112
+ i += 1
113
+ end
114
+ else
115
+ table_alias
116
+ end
117
+ end
118
+ end
119
+ end
@@ -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
@@ -1,5 +1,11 @@
1
1
  module Sequel
2
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
+
3
9
  PREPARED_ARG_PLACEHOLDER = LiteralString.new('?').freeze
4
10
 
5
11
  # Default implementation of the argument mapper to allow
@@ -1,5 +1,28 @@
1
1
  module Sequel
2
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
+
3
26
  # Adds an further filter to an existing filter using AND. If no filter
4
27
  # exists an error is raised. This method is identical to #filter except
5
28
  # it expects an existing filter.
@@ -9,6 +32,16 @@ module Sequel
9
32
  raise(InvalidOperation, "No existing filter found.") unless @opts[:having] || @opts[:where]
10
33
  filter(*cond, &block)
11
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
12
45
 
13
46
  # Returns a copy of the dataset with the SQL DISTINCT clause.
14
47
  # The DISTINCT clause is used to remove duplicate rows from the
@@ -74,7 +107,8 @@ module Sequel
74
107
  #
75
108
  # filter also takes a block, which should return one of the above argument
76
109
  # types, and is treated the same way. This block yields a virtual row object,
77
- # which is easy to use to create identifiers and functions.
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]
78
112
  #
79
113
  # If both a block and regular argument
80
114
  # are provided, they get ANDed together.
@@ -100,7 +134,7 @@ module Sequel
100
134
  # software.filter{|o| o.price < 100}.sql #=>
101
135
  # "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
102
136
  #
103
- # See doc/dataset_filtering.rdoc for more examples and details.
137
+ # See the the {"Dataset Filtering" guide}[link:files/doc/dataset_filtering_rdoc.html] for more examples and details.
104
138
  def filter(*cond, &block)
105
139
  _filter(@opts[:having] ? :having : :where, *cond, &block)
106
140
  end
@@ -175,6 +209,19 @@ module Sequel
175
209
  clone(:group => (columns.compact.empty? ? nil : columns))
176
210
  end
177
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
178
225
 
179
226
  # Returns a copy of the dataset with the HAVING conditions changed. See #filter for argument types.
180
227
  #
@@ -213,6 +260,100 @@ module Sequel
213
260
  clone(o)
214
261
  end
215
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
+
216
357
  # If given an integer, the dataset will contain only the first l results.
217
358
  # If given a range, it will contain only those at offsets within that
218
359
  # range. If a second argument is given, it is used as an offset.
@@ -249,6 +390,14 @@ module Sequel
249
390
  clone(:lock => style)
250
391
  end
251
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
+
252
401
  # Adds an alternate filter to an existing filter using OR. If no filter
253
402
  # exists an error is raised.
254
403
  #
@@ -289,6 +438,35 @@ module Sequel
289
438
  order(*columns, &block)
290
439
  end
291
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
+
292
470
  # Returns a copy of the dataset with the order reversed. If no order is
293
471
  # given, the existing order is inverted.
294
472
  def reverse_order(*order)
@@ -318,17 +496,51 @@ module Sequel
318
496
  def select_all
319
497
  clone(:select => nil)
320
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
321
512
 
322
513
  # Returns a copy of the dataset with the given columns added
323
- # to the existing selected columns.
514
+ # to the existing selected columns. If no columns are currently selected
515
+ # it will just select the columns given.
324
516
  #
325
517
  # dataset.select(:a).select(:b) # SELECT b FROM items
326
518
  # dataset.select(:a).select_more(:b) # SELECT a, b FROM items
519
+ # dataset.select_more(:b) # SELECT b FROM items
327
520
  def select_more(*columns, &block)
328
521
  columns = @opts[:select] + columns if @opts[:select]
329
522
  select(*columns, &block)
330
523
  end
331
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
+
332
544
  # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
333
545
  #
334
546
  # dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
@@ -370,15 +582,69 @@ module Sequel
370
582
  def unordered
371
583
  order(nil)
372
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
373
635
 
374
636
  private
375
637
 
376
638
  # Internal filter method so it works on either the having or where clauses.
377
639
  def _filter(clause, *cond, &block)
378
640
  cond = cond.first if cond.size == 1
379
- cond = filter_expr(cond, &block)
380
- cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
381
- clone(clause => cond)
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
382
648
  end
383
649
 
384
650
  # Add the dataset to the list of compounds