sequel_core 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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