sequel_core 2.0.1 → 2.1.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 (40) hide show
  1. data/CHANGELOG +50 -0
  2. data/README +1 -2
  3. data/Rakefile +1 -1
  4. data/bin/sequel +26 -11
  5. data/doc/dataset_filtering.rdoc +9 -2
  6. data/lib/sequel_core/adapters/adapter_skeleton.rb +0 -2
  7. data/lib/sequel_core/adapters/mysql.rb +11 -97
  8. data/lib/sequel_core/adapters/odbc_mssql.rb +1 -1
  9. data/lib/sequel_core/adapters/oracle.rb +1 -1
  10. data/lib/sequel_core/adapters/postgres.rb +33 -17
  11. data/lib/sequel_core/adapters/sqlite.rb +1 -1
  12. data/lib/sequel_core/connection_pool.rb +12 -35
  13. data/lib/sequel_core/core_ext.rb +5 -19
  14. data/lib/sequel_core/core_sql.rb +17 -6
  15. data/lib/sequel_core/database.rb +14 -2
  16. data/lib/sequel_core/dataset/callback.rb +0 -3
  17. data/lib/sequel_core/dataset/convenience.rb +4 -3
  18. data/lib/sequel_core/dataset/parse_tree_sequelizer.rb +0 -21
  19. data/lib/sequel_core/dataset/sequelizer.rb +42 -43
  20. data/lib/sequel_core/dataset/sql.rb +121 -62
  21. data/lib/sequel_core/dataset.rb +20 -4
  22. data/lib/sequel_core/deprecated.rb +0 -6
  23. data/lib/sequel_core/migration.rb +4 -0
  24. data/lib/sequel_core/object_graph.rb +8 -6
  25. data/lib/sequel_core/pretty_table.rb +1 -1
  26. data/lib/sequel_core/schema/sql.rb +2 -0
  27. data/lib/sequel_core/sql.rb +393 -153
  28. data/lib/sequel_core.rb +22 -7
  29. data/spec/adapters/mysql_spec.rb +11 -7
  30. data/spec/adapters/postgres_spec.rb +32 -4
  31. data/spec/blockless_filters_spec.rb +33 -6
  32. data/spec/connection_pool_spec.rb +18 -16
  33. data/spec/core_ext_spec.rb +0 -13
  34. data/spec/core_sql_spec.rb +59 -13
  35. data/spec/database_spec.rb +5 -5
  36. data/spec/dataset_spec.rb +167 -55
  37. data/spec/object_graph_spec.rb +9 -4
  38. data/spec/sequelizer_spec.rb +8 -17
  39. data/spec/spec_helper.rb +4 -3
  40. metadata +2 -2
@@ -138,7 +138,7 @@ module Sequel
138
138
 
139
139
  private
140
140
  def connection_pool_default_options
141
- o = super.merge(:pool_reuse_connections=>:always, :pool_convert_exceptions=>false)
141
+ o = super.merge(:pool_convert_exceptions=>false)
142
142
  # Default to only a single connection if a memory database is used,
143
143
  # because otherwise each connection will get a separate database
144
144
  o[:max_connections] = 1 if @opts[:database] == ':memory:' || @opts[:database].blank?
@@ -1,8 +1,9 @@
1
1
  # A ConnectionPool manages access to database connections by keeping
2
2
  # multiple connections and giving threads exclusive access to each
3
3
  # connection.
4
- class ConnectionPool
5
- # An array of connections currently being used
4
+ class Sequel::ConnectionPool
5
+ # A hash of connections currently being used, key is the Thread,
6
+ # value is the connection.
6
7
  attr_reader :allocated
7
8
 
8
9
  # An array of connections opened but not currently used
@@ -42,13 +43,6 @@ class ConnectionPool
42
43
  # will open (default 4)
43
44
  # * :pool_convert_exceptions - Whether to convert non-StandardError based exceptions
44
45
  # to RuntimeError exceptions (default true)
45
- # * :pool_reuse_connections - Which strategy to follow in regards to reusing connections:
46
- # * :always - Always reuse a connection that belongs to the same thread
47
- # * :allow - Only reuse a connection that belongs to the same thread if
48
- # another cannot be acquired immediately (default)
49
- # * :last_resort - Only reuse a connection that belongs to the same thread if
50
- # the pool timeout has expired
51
- # * :never - Never reuse a connection that belongs to the same thread
52
46
  # * :pool_sleep_time - The amount of time to sleep before attempting to acquire
53
47
  # a connection again (default 0.001)
54
48
  # * :pool_timeout - The amount of seconds to wait to acquire a connection
@@ -59,11 +53,10 @@ class ConnectionPool
59
53
  @connection_proc = block
60
54
 
61
55
  @available_connections = []
62
- @allocated = []
56
+ @allocated = {}
63
57
  @created_count = 0
64
58
  @timeout = opts[:pool_timeout] || 5
65
59
  @sleep_time = opts[:pool_sleep_time] || 0.001
66
- @reuse_connections = opts[:pool_reuse_connections] || :allow
67
60
  @convert_exceptions = opts.include?(:pool_convert_exceptions) ? opts[:pool_convert_exceptions] : true
68
61
  end
69
62
 
@@ -72,10 +65,8 @@ class ConnectionPool
72
65
  #
73
66
  # pool.hold {|conn| conn.execute('DROP TABLE posts')}
74
67
  #
75
- # Pool#hold can be re-entrant, meaning it can be called recursively in
76
- # the same thread without blocking if the :pool_reuse_connections option
77
- # was set to :always or :allow. Depending on the :pool_reuse_connections
78
- # option you may get the connection currently used by the thread or a new connection.
68
+ # Pool#hold is re-entrant, meaning it can be called recursively in
69
+ # the same thread without blocking.
79
70
  #
80
71
  # If no connection is immediately available and the pool is already using the maximum
81
72
  # number of connections, Pool#hold will block until a connection
@@ -88,21 +79,11 @@ class ConnectionPool
88
79
  time = Time.new
89
80
  timeout = time + @timeout
90
81
  sleep_time = @sleep_time
91
- reuse = @reuse_connections
92
- if (reuse == :always) && (conn = owned_connection(t))
82
+ if conn = owned_connection(t)
93
83
  return yield(conn)
94
84
  end
95
- reuse = reuse == :allow ? true : false
96
85
  until conn = acquire(t)
97
- if reuse && (conn = owned_connection(t))
98
- return yield(conn)
99
- end
100
- if Time.new > timeout
101
- if (@reuse_connections == :last_resort) && (conn = owned_connection(t))
102
- return yield(conn)
103
- end
104
- raise(::Sequel::Error::PoolTimeoutError)
105
- end
86
+ raise(::Sequel::Error::PoolTimeoutError) if Time.new > timeout
106
87
  sleep sleep_time
107
88
  end
108
89
  begin
@@ -131,18 +112,14 @@ class ConnectionPool
131
112
 
132
113
  # Returns the connection owned by the supplied thread, if any.
133
114
  def owned_connection(thread)
134
- @mutex.synchronize do
135
- x = @allocated.assoc(thread)
136
- x[1] if x
137
- end
115
+ @mutex.synchronize{@allocated[thread]}
138
116
  end
139
117
 
140
118
  # Assigns a connection to the supplied thread, if one is available.
141
119
  def acquire(thread)
142
120
  @mutex.synchronize do
143
121
  if conn = available
144
- @allocated << [thread, conn]
145
- conn
122
+ @allocated[thread] = conn
146
123
  end
147
124
  end
148
125
  end
@@ -166,7 +143,7 @@ class ConnectionPool
166
143
  # Releases the connection assigned to the supplied thread.
167
144
  def release(thread, conn)
168
145
  @mutex.synchronize do
169
- @allocated.delete([thread, conn])
146
+ @allocated.delete(thread)
170
147
  @available_connections << conn
171
148
  end
172
149
  end
@@ -178,7 +155,7 @@ end
178
155
  #
179
156
  # Note that using a single threaded pool with some adapters can cause
180
157
  # errors in certain cases, see Sequel.single_threaded=.
181
- class SingleThreadedPool
158
+ class Sequel::SingleThreadedPool
182
159
  # The single database connection for the pool
183
160
  attr_reader :conn
184
161
 
@@ -1,7 +1,3 @@
1
- # This file includes augmentations to the core ruby classes the Sequel uses,
2
- # which are unrelated to the creation of SQL. It includes common
3
- # idioms to reduce the amount of code duplication.
4
-
5
1
  class Array
6
2
  # True if the array is not empty and all of its elements are
7
3
  # arrays of size 2. This is used to determine if the array
@@ -65,7 +61,7 @@ class Module
65
61
  # alias_method to, from
66
62
  # end
67
63
  def metaalias(to, from)
68
- metaclass.instance_eval{alias_method to, from}
64
+ meta_eval{alias_method to, from}
69
65
  end
70
66
 
71
67
  # Make a singleton/class attribute accessor method(s).
@@ -75,7 +71,7 @@ class Module
75
71
  # attr_accessor *meths
76
72
  # end
77
73
  def metaattr_accessor(*meths)
78
- metaclass.instance_eval{attr_accessor(*meths)}
74
+ meta_eval{attr_accessor(*meths)}
79
75
  end
80
76
 
81
77
  # Make a singleton/class method(s) private.
@@ -86,17 +82,7 @@ class Module
86
82
  # attr_reader *meths
87
83
  # end
88
84
  def metaattr_reader(*meths)
89
- metaclass.instance_eval{attr_reader(*meths)}
90
- end
91
-
92
- # Make a singleton/class method(s) private.
93
- # Replaces the construct:
94
- #
95
- # class << self
96
- # private *meths
97
- # end
98
- def metaprivate(*meths)
99
- metaclass.instance_eval{private(*meths)}
85
+ meta_eval{attr_reader(*meths)}
100
86
  end
101
87
  end
102
88
 
@@ -184,10 +170,10 @@ class String
184
170
  end
185
171
 
186
172
  # Converts a string into a Time or DateTime object, depending on the
187
- # value of Sequel.time_class
173
+ # value of Sequel.datetime_class
188
174
  def to_sequel_time
189
175
  begin
190
- Sequel.time_class.parse(self)
176
+ Sequel.datetime_class.parse(self)
191
177
  rescue => e
192
178
  raise Sequel::Error::InvalidValue, "Invalid time value '#{self}' (#{e.message})"
193
179
  end
@@ -1,6 +1,3 @@
1
- # This file holds the extensions to core ruby classes that relate to the creation of SQL
2
- # code.
3
-
4
1
  class Array
5
2
  # Return a Sequel::SQL::BooleanExpression created from this array, not matching any of the
6
3
  # conditions.
@@ -8,6 +5,12 @@ class Array
8
5
  sql_expr_if_all_two_pairs(:OR, true)
9
6
  end
10
7
 
8
+ # Return a Sequel::SQL::CaseExpression with this array as the conditions and the given
9
+ # default value.
10
+ def case(default)
11
+ ::Sequel::SQL::CaseExpression.new(self, default)
12
+ end
13
+
11
14
  # Return a Sequel::SQL::BooleanExpression created from this array, matching all of the
12
15
  # conditions.
13
16
  def sql_expr
@@ -81,6 +84,13 @@ class Hash
81
84
  ::Sequel::SQL::BooleanExpression.from_value_pairs(self, :OR, true)
82
85
  end
83
86
 
87
+ # Return a Sequel::SQL::CaseExpression with this hash as the conditions and the given
88
+ # default value. Note that the order of the conditions will be arbitrary, so all
89
+ # conditions should be orthogonal.
90
+ def case(default)
91
+ ::Sequel::SQL::CaseExpression.new(to_a, default)
92
+ end
93
+
84
94
  # Return a Sequel::SQL::BooleanExpression created from this hash, matching all of the
85
95
  # conditions.
86
96
  def sql_expr
@@ -101,7 +111,8 @@ class Hash
101
111
  end
102
112
 
103
113
  class String
104
- include Sequel::SQL::ColumnMethods
114
+ include Sequel::SQL::AliasMethods
115
+ include Sequel::SQL::CastMethods
105
116
 
106
117
  # Converts a string into an LiteralString, in order to override string
107
118
  # literalization, e.g.:
@@ -131,8 +142,8 @@ class String
131
142
  end
132
143
 
133
144
  class Symbol
134
- include Sequel::SQL::ColumnMethods
135
- include Sequel::SQL::ComplexExpressionMethods
145
+ include Sequel::SQL::QualifyingMethods
146
+ include Sequel::SQL::GenericExpressionMethods
136
147
 
137
148
  # If no argument is given, returns a Sequel::SQL::ColumnAll object specifying all
138
149
  # columns for this table.
@@ -172,7 +172,8 @@ module Sequel
172
172
  @scheme = scheme
173
173
  @@adapters[scheme.to_sym] = self
174
174
  end
175
- metaprivate :set_adapter_scheme
175
+
176
+ private_class_method :set_adapter_scheme
176
177
 
177
178
  ### Instance Methods ###
178
179
 
@@ -378,6 +379,17 @@ module Sequel
378
379
  value.to_s
379
380
  when :float
380
381
  Float(value)
382
+ when :decimal
383
+ case value
384
+ when BigDecimal
385
+ value
386
+ when String, Float
387
+ value.to_d
388
+ when Integer
389
+ value.to_s.to_d
390
+ else
391
+ raise ArgumentError, "invalid value for BigDecimal: #{value.inspect}"
392
+ end
381
393
  when :boolean
382
394
  case value
383
395
  when false, 0, "0", /\Af(alse)?\z/i
@@ -407,7 +419,7 @@ module Sequel
407
419
  end
408
420
  when :datetime
409
421
  raise(ArgumentError, "invalid value for #{tc}: #{value.inspect}") unless value.is_one_of?(DateTime, Date, Time, String)
410
- if Sequel.time_class === value
422
+ if Sequel.datetime_class === value
411
423
  # Already the correct class, no need to convert
412
424
  value
413
425
  else
@@ -1,6 +1,3 @@
1
- # This file holds empty methods that can be
2
- # overridden to provide callback behavior.
3
-
4
1
  module Sequel
5
2
  class Dataset
6
3
  private
@@ -233,10 +233,11 @@ module Sequel
233
233
 
234
234
  # Returns a hash with one column used as key and another used as value.
235
235
  # If rows have duplicate values for the key column, the latter row(s)
236
- # will overwrite the value of the previous row(s).
237
- def to_hash(key_column, value_column)
236
+ # will overwrite the value of the previous row(s). If the value_column
237
+ # is not given or nil, uses the entire hash as the value.
238
+ def to_hash(key_column, value_column = nil)
238
239
  inject({}) do |m, r|
239
- m[r[key_column]] = r[value_column]
240
+ m[r[key_column]] = value_column ? r[value_column] : r
240
241
  m
241
242
  end
242
243
  end
@@ -1,24 +1,3 @@
1
- # This file includes dataset methods for translating Ruby expressions
2
- # into SQL expressions, making it possible to specify dataset filters using
3
- # blocks, e.g.:
4
- #
5
- # DB[:items].filter {:price < 100}
6
- # DB[:items].filter {:category == 'ruby' && :date < Date.today - 7}
7
- #
8
- # Block filters can refer to literals, variables, constants, arguments,
9
- # instance variables or anything else in order to create parameterized
10
- # queries. Block filters can also refer to other dataset objects as
11
- # sub-queries. Block filters are pretty much limitless!
12
- #
13
- # Block filters are based on ParseTree. If you do not have the ParseTree
14
- # gem installed, block filters will raise an error.
15
- #
16
- # To enable full block filter support make sure you have both ParseTree and
17
- # Ruby2Ruby installed:
18
- #
19
- # sudo gem install parsetree
20
- # sudo gem install ruby2ruby
21
-
22
1
  module Sequel
23
2
  class Dataset
24
3
  private
@@ -1,51 +1,50 @@
1
- begin
2
- require 'parse_tree'
3
- require 'sequel_core/dataset/parse_tree_sequelizer'
4
- class Proc
5
- def to_sql(dataset, opts = {})
6
- dataset.send(:pt_expr, to_sexp[2], self.binding, opts)
7
- end
8
- end
1
+ unless defined?(SEQUEL_NO_PARSE_TREE)
9
2
  begin
10
- require 'ruby2ruby'
11
- class Sequel::Dataset
12
- # Evaluates a method call. This method is used to evaluate Ruby expressions
13
- # referring to indirect values, e.g.:
14
- #
15
- # dataset.filter {:category => category.to_s}
16
- # dataset.filter {:x > y[0..3]}
17
- #
18
- # This method depends on the Ruby2Ruby gem. If you do not have Ruby2Ruby
19
- # installed, this method will raise an error.
20
- def ext_expr(e, b, opts)
21
- eval(::RubyToRuby.new.process(e), b)
22
- end
23
- end
3
+ require 'parse_tree'
4
+ require 'sequel_core/dataset/parse_tree_sequelizer'
24
5
  class Proc
25
- remove_method :to_sexp
26
- end
27
- rescue LoadError
28
- class Sequel::Dataset
29
- def ext_expr(*args)
30
- raise Sequel::Error, "You must have the Ruby2Ruby gem installed in order to use this block filter."
6
+ def to_sql(dataset, opts = {})
7
+ Sequel::Deprecation.deprecate("ParseTree filters are deprecated and will be removed in Sequel 2.2")
8
+ dataset.send(:pt_expr, to_sexp[2], self.binding, opts)
31
9
  end
32
10
  end
33
- ensure
34
- class Proc
35
- # replacement for Proc#to_sexp as defined in ruby2ruby.
36
- # see also: http://rubyforge.org/tracker/index.php?func=detail&aid=18095&group_id=1513&atid=5921
37
- # The ruby2ruby implementation leaks memory, so we fix it.
38
- def to_sexp
39
- block = self
40
- c = Class.new {define_method(:m, &block)}
41
- ParseTree.translate(c, :m)[2]
11
+ begin
12
+ require 'ruby2ruby'
13
+ class Sequel::Dataset
14
+ # Evaluates a method call. This method is used to evaluate Ruby expressions
15
+ # referring to indirect values, e.g.:
16
+ #
17
+ # dataset.filter {:category => category.to_s}
18
+ # dataset.filter {:x > y[0..3]}
19
+ #
20
+ # This method depends on the Ruby2Ruby gem. If you do not have Ruby2Ruby
21
+ # installed, this method will raise an error.
22
+ def ext_expr(e, b, opts)
23
+ eval(::RubyToRuby.new.process(e), b)
24
+ end
25
+ end
26
+ class Proc
27
+ remove_method :to_sexp
28
+ end
29
+ rescue LoadError
30
+ class Sequel::Dataset
31
+ def ext_expr(*args)
32
+ raise Sequel::Error, "You must have the Ruby2Ruby gem installed in order to use this block filter."
33
+ end
34
+ end
35
+ ensure
36
+ class Proc
37
+ # replacement for Proc#to_sexp as defined in ruby2ruby.
38
+ # see also: http://rubyforge.org/tracker/index.php?func=detail&aid=18095&group_id=1513&atid=5921
39
+ # The ruby2ruby implementation leaks memory, so we fix it.
40
+ def to_sexp
41
+ block = self
42
+ c = Class.new {define_method(:m, &block)}
43
+ ParseTree.translate(c, :m)[2]
44
+ end
42
45
  end
43
46
  end
44
- end
45
- rescue LoadError
46
- class Proc
47
- def to_sql(*args)
48
- raise Sequel::Error, "You must have the ParseTree gem installed in order to use block filters."
49
- end
47
+ rescue LoadError
48
+ SEQUEL_NO_PARSE_TREE = true
50
49
  end
51
50
  end
@@ -1,6 +1,3 @@
1
- # This file includes all the dataset methods concerned with
2
- # generating SQL statements for retrieving and manipulating records.
3
-
4
1
  module Sequel
5
2
  class Dataset
6
3
  AND_SEPARATOR = " AND ".freeze
@@ -9,13 +6,8 @@ module Sequel
9
6
  COLUMN_REF_RE1 = /\A([\w ]+)__([\w ]+)___([\w ]+)\z/.freeze
10
7
  COLUMN_REF_RE2 = /\A([\w ]+)___([\w ]+)\z/.freeze
11
8
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
9
+ COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql]
12
10
  DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
13
- JOIN_TYPES = {
14
- :left_outer => 'LEFT OUTER JOIN'.freeze,
15
- :right_outer => 'RIGHT OUTER JOIN'.freeze,
16
- :full_outer => 'FULL OUTER JOIN'.freeze,
17
- :inner => 'INNER JOIN'.freeze
18
- }
19
11
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
20
12
  NULL = "NULL".freeze
21
13
  QUESTION_MARK = '?'.freeze
@@ -32,17 +24,21 @@ module Sequel
32
24
  filter(*cond, &block)
33
25
  end
34
26
 
27
+ # SQL fragment for the aliased expression
28
+ def aliased_expression_sql(ae)
29
+ "#{literal(ae.expression)} AS #{quote_identifier(ae.aliaz)}"
30
+ end
31
+
32
+ # SQL fragment for specifying given CaseExpression.
33
+ def case_expression_sql(ce)
34
+ "(CASE #{ce.conditions.collect{|c,r| "WHEN #{literal(c)} THEN #{literal(r)} "}.join}ELSE #{literal(ce.default)} END)"
35
+ end
36
+
35
37
  # SQL fragment for specifying all columns in a given table.
36
38
  def column_all_sql(ca)
37
39
  "#{quote_identifier(ca.table)}.*"
38
40
  end
39
41
 
40
- # SQL fragment for column expressions
41
- def column_expr_sql(ce)
42
- r = ce.r
43
- "#{literal(ce.l)} #{ce.op}#{" #{literal(r)}" if r}"
44
- end
45
-
46
42
  # SQL fragment for complex expressions
47
43
  def complex_expression_sql(op, args)
48
44
  case op
@@ -54,6 +50,8 @@ module Sequel
54
50
  "NOT #{literal(args.at(0))}"
55
51
  when :NOOP
56
52
  literal(args.at(0))
53
+ when :'B~'
54
+ "~#{literal(args.at(0))}"
57
55
  else
58
56
  raise(Sequel::Error, "invalid operator #{op}")
59
57
  end
@@ -61,11 +59,7 @@ module Sequel
61
59
 
62
60
  # Returns the number of records in the dataset.
63
61
  def count
64
- if @opts[:sql] || @opts[:group]
65
- from_self.count
66
- else
67
- single_value(STOCK_COUNT_OPTS).to_i
68
- end
62
+ options_overlap(COUNT_FROM_SELF_OPTS) ? from_self.count : single_value(STOCK_COUNT_OPTS).to_i
69
63
  end
70
64
  alias_method :size, :count
71
65
 
@@ -108,7 +102,7 @@ module Sequel
108
102
  clause = (@opts[:having] ? :having : :where)
109
103
  cond = cond.first if cond.size == 1
110
104
  cond = cond.sql_or if (Hash === cond) || ((Array === cond) && (cond.all_two_pairs?))
111
- cond = filter_expr(block || cond)
105
+ cond = filter_expr(cond, &block)
112
106
  cond = SQL::BooleanExpression.invert(cond)
113
107
  cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
114
108
  clone(clause => cond)
@@ -168,9 +162,8 @@ module Sequel
168
162
  def filter(*cond, &block)
169
163
  clause = (@opts[:having] ? :having : :where)
170
164
  cond = cond.first if cond.size == 1
171
- raise(Error::InvalidFilter, "Invalid filter specified. Did you mean to supply a block?") if cond === true || cond === false
172
165
  cond = transform_save(cond) if @transform if cond.is_a?(Hash)
173
- cond = filter_expr(block || cond)
166
+ cond = filter_expr(cond, &block)
174
167
  cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause] && !@opts[clause].blank?
175
168
  clone(clause => cond)
176
169
  end
@@ -187,6 +180,9 @@ module Sequel
187
180
  case s = source.first
188
181
  when Hash
189
182
  s.values.first
183
+ when Symbol
184
+ sch, table, aliaz = split_symbol(s)
185
+ aliaz ? aliaz.to_sym : s
190
186
  else
191
187
  s
192
188
  end
@@ -319,6 +315,30 @@ module Sequel
319
315
  clone(o)
320
316
  end
321
317
 
318
+ # SQL fragment specifying an Irregular (cast/extract) SQL function call
319
+ def irregular_function_sql(f)
320
+ "#{f.f}(#{literal(f.arg1)} #{f.joiner} #{literal(f.arg2)})"
321
+ end
322
+
323
+ # SQL fragment specifying a JOIN clause without ON or USING.
324
+ def join_clause_sql(jc)
325
+ table = jc.table
326
+ table_alias = jc.table_alias
327
+ table_alias = nil if table == table_alias
328
+ " #{join_type_sql(jc.join_type)} #{table_ref(table)}" \
329
+ "#{" AS #{quote_identifier(jc.table_alias)}" if table_alias}"
330
+ end
331
+
332
+ # SQL fragment specifying a JOIN clause with ON.
333
+ def join_on_clause_sql(jc)
334
+ "#{join_clause_sql(jc)} ON #{literal(filter_expr(jc.on))}"
335
+ end
336
+
337
+ # SQL fragment specifying a JOIN clause with USING.
338
+ def join_using_clause_sql(jc)
339
+ "#{join_clause_sql(jc)} USING (#{column_list(jc.using)})"
340
+ end
341
+
322
342
  # Returns a joined dataset. Uses the following arguments:
323
343
  #
324
344
  # * type - The type of join to do (:inner, :left_outer, :right_outer, :full)
@@ -326,39 +346,59 @@ module Sequel
326
346
  # * Dataset - a subselect is performed with an alias of tN for some value of N
327
347
  # * Model (or anything responding to :table_name) - table.table_name
328
348
  # * String, Symbol: table
329
- # * expr - Depends on type:
330
- # * Hash, Array - Assumes key (1st arg) is column of joined table (unless already
349
+ # * expr - specifies conditions, depends on type:
350
+ # * Hash, Array with all two pairs - Assumes key (1st arg) is column of joined table (unless already
331
351
  # qualified), and value (2nd arg) is column of the last joined or primary table.
332
352
  # To specify multiple conditions on a single joined table column, you must use an array.
333
- # * Symbol - Assumed to be a column in the joined table that points to the id
334
- # column in the last joined or primary table.
353
+ # Uses a JOIN with an ON clause.
354
+ # * Array - If all members of the array are symbols, considers them as columns and
355
+ # uses a JOIN with a USING clause. Most databases will remove duplicate columns from
356
+ # the result set if this is used.
357
+ # * nil - If a block is not given, doesn't use ON or USING, so the JOIN should be a NATURAL
358
+ # or CROSS join. If a block is given, uses a ON clause based on the block, see below.
359
+ # * Everything else - pretty much the same as a using the argument in a call to filter,
360
+ # so strings are considered literal, symbols specify boolean columns, and blockless
361
+ # filter expressions can be used. Uses a JOIN with an ON clause.
335
362
  # * table_alias - the name of the table's alias when joining, necessary for joining
336
363
  # to the same table more than once. No alias is used by default.
337
- def join_table(type, table, expr=nil, table_alias=nil)
338
- raise(Error::InvalidJoinType, "Invalid join type: #{type}") unless join_type = JOIN_TYPES[type || :inner]
339
-
340
- table = if Dataset === table
341
- table_alias = unless table_alias
364
+ # * block - The block argument should only be given if a JOIN with an ON clause is used,
365
+ # in which case it yields the table alias/name for the table currently being joined,
366
+ # the table alias/name for the last joined (or first table), and an array of previous
367
+ # SQL::JoinClause.
368
+ def join_table(type, table, expr=nil, table_alias=nil, &block)
369
+ if Dataset === table
370
+ if table_alias.nil?
342
371
  table_alias_num = (@opts[:num_dataset_sources] || 0) + 1
343
- "t#{table_alias_num}"
372
+ table_alias = "t#{table_alias_num}"
344
373
  end
345
- table.to_table_reference
374
+ table_name = table_alias
346
375
  else
347
376
  table = table.table_name if table.respond_to?(:table_name)
348
- table_alias ||= table
349
- table_ref(table)
377
+ table_name = table_alias || table
350
378
  end
351
379
 
352
- expr = [[expr, :id]] unless expr.is_one_of?(Hash, Array)
353
- join_conditions = expr.collect do |k, v|
354
- k = qualified_column_name(k, table_alias) if k.is_a?(Symbol)
355
- v = qualified_column_name(v, @opts[:last_joined_table] || first_source) if v.is_a?(Symbol)
356
- [k,v]
380
+ join = if expr.nil? and !block_given?
381
+ SQL::JoinClause.new(type, table, table_alias)
382
+ elsif Array === expr and !expr.empty? and expr.all?{|x| Symbol === x}
383
+ raise(Sequel::Error, "can't use a block if providing an array of symbols as expr") if block_given?
384
+ SQL::JoinUsingClause.new(expr, type, table, table_alias)
385
+ else
386
+ last_alias = @opts[:last_joined_table] || first_source
387
+ if Hash === expr or (Array === expr and expr.all_two_pairs?)
388
+ expr = expr.collect do |k, v|
389
+ k = qualified_column_name(k, table_name) if k.is_a?(Symbol)
390
+ v = qualified_column_name(v, last_alias) if v.is_a?(Symbol)
391
+ [k,v]
392
+ end
393
+ end
394
+ if block_given?
395
+ expr2 = yield(table_name, last_alias, @opts[:join] || [])
396
+ expr = expr ? SQL::BooleanExpression.new(:AND, expr, expr2) : expr2
397
+ end
398
+ SQL::JoinOnClause.new(expr, type, table, table_alias)
357
399
  end
358
400
 
359
- quoted_table_alias = quote_identifier(table_alias)
360
- clause = "#{@opts[:join]} #{join_type} #{table}#{" #{quoted_table_alias}" if quoted_table_alias != table} ON #{literal(filter_expr(join_conditions))}"
361
- opts = {:join => clause, :last_joined_table => table_alias}
401
+ opts = {:join => (@opts[:join] || []) + [join], :last_joined_table => table_name}
362
402
  opts[:num_dataset_sources] = table_alias_num if table_alias_num
363
403
  clone(opts)
364
404
  end
@@ -449,7 +489,7 @@ module Sequel
449
489
  clause = (@opts[:having] ? :having : :where)
450
490
  cond = cond.first if cond.size == 1
451
491
  if @opts[clause]
452
- clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(block || cond)))
492
+ clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(cond, &block)))
453
493
  else
454
494
  raise Error::NoExistingFilter, "No existing filter found."
455
495
  end
@@ -478,9 +518,15 @@ module Sequel
478
518
  order(*((@opts[:order] || []) + order))
479
519
  end
480
520
 
481
- # SQL fragment for the qualifed column reference, specifying
482
- # a table and a column.
483
- def qualified_column_ref_sql(qcr)
521
+ # SQL fragment for the ordered expression, used in the ORDER BY
522
+ # clause.
523
+ def ordered_expression_sql(oe)
524
+ "#{literal(oe.expression)} #{oe.descending ? 'DESC' : 'ASC'}"
525
+ end
526
+
527
+ # SQL fragment for the qualifed identifier, specifying
528
+ # a table and a column (or schema and table).
529
+ def qualified_identifier_sql(qcr)
484
530
  "#{quote_identifier(qcr.table)}.#{quote_identifier(qcr.column)}"
485
531
  end
486
532
 
@@ -553,7 +599,7 @@ module Sequel
553
599
  end
554
600
 
555
601
  if join = opts[:join]
556
- sql << join
602
+ join.each{|j| sql << literal(j)}
557
603
  end
558
604
 
559
605
  if where = opts[:where]
@@ -564,14 +610,14 @@ module Sequel
564
610
  sql << " GROUP BY #{column_list(group)}"
565
611
  end
566
612
 
567
- if order = opts[:order]
568
- sql << " ORDER BY #{column_list(order)}"
569
- end
570
-
571
613
  if having = opts[:having]
572
614
  sql << " HAVING #{literal(having)}"
573
615
  end
574
616
 
617
+ if order = opts[:order]
618
+ sql << " ORDER BY #{column_list(order)}"
619
+ end
620
+
575
621
  if limit = opts[:limit]
576
622
  sql << " LIMIT #{limit}"
577
623
  if offset = opts[:offset]
@@ -683,7 +729,7 @@ module Sequel
683
729
  end
684
730
 
685
731
  [:inner, :full_outer, :right_outer, :left_outer].each do |jtype|
686
- define_method("#{jtype}_join"){|*args| join_table(jtype, *args)}
732
+ class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end")
687
733
  end
688
734
  alias_method :join, :inner_join
689
735
 
@@ -711,7 +757,13 @@ module Sequel
711
757
  end
712
758
 
713
759
  # SQL fragment based on the expr type. See #filter.
714
- def filter_expr(expr)
760
+ def filter_expr(expr = nil, &block)
761
+ expr = nil if expr == []
762
+ if expr && block
763
+ return SQL::BooleanExpression.new(:AND, filter_expr(expr), filter_expr(block))
764
+ elsif block
765
+ expr = block
766
+ end
715
767
  case expr
716
768
  when Hash
717
769
  SQL::BooleanExpression.from_value_pairs(expr)
@@ -722,11 +774,13 @@ module Sequel
722
774
  SQL::BooleanExpression.from_value_pairs(expr)
723
775
  end
724
776
  when Proc
725
- expr.to_sql(self).lit
777
+ Sequel.use_parse_tree ? expr.to_sql(self).lit : filter_expr(expr.call)
726
778
  when SQL::NumericExpression, SQL::StringExpression
727
779
  raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
728
780
  when Symbol, SQL::Expression
729
781
  expr
782
+ when TrueClass, FalseClass
783
+ SQL::BooleanExpression.new(:NOOP, expr)
730
784
  when String
731
785
  "(#{expr})".lit
732
786
  else
@@ -749,16 +803,21 @@ module Sequel
749
803
  return nil unless order
750
804
  new_order = []
751
805
  order.map do |f|
752
- if f.is_a?(SQL::ColumnExpr) && (f.op == SQL::ColumnMethods::DESC)
753
- f.l
754
- elsif f.is_a?(SQL::ColumnExpr) && (f.op == SQL::ColumnMethods::ASC)
755
- f.l.desc
806
+ case f
807
+ when SQL::OrderedExpression
808
+ SQL::OrderedExpression.new(f.expression, !f.descending)
756
809
  else
757
- f.desc
810
+ SQL::OrderedExpression.new(f)
758
811
  end
759
812
  end
760
813
  end
761
814
 
815
+ # SQL fragment specifying a JOIN type, converts underscores to
816
+ # spaces and upcases.
817
+ def join_type_sql(join_type)
818
+ "#{join_type.to_s.gsub('_', ' ').upcase} JOIN"
819
+ end
820
+
762
821
  # Returns a qualified column name (including a table name) if the column
763
822
  # name isn't already qualified.
764
823
  def qualified_column_name(column, table)
@@ -766,7 +825,7 @@ module Sequel
766
825
  c_table, column, c_alias = split_symbol(column)
767
826
  schema, table, t_alias = split_symbol(table) if Symbol === table
768
827
  c_table ||= t_alias || table
769
- ::Sequel::SQL::QualifiedColumnRef.new(c_table, column)
828
+ ::Sequel::SQL::QualifiedIdentifier.new(c_table, column)
770
829
  else
771
830
  column
772
831
  end