sequel 0.1.9.12 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,55 @@
1
+ === 0.2.0 (2007-09-02)
2
+
3
+ * Fixed Model.drop_table (thanks Duane Johnson.)
4
+
5
+ * Dataset#each can now return rows for arbitrary SQL by specifying :sql option.
6
+
7
+ * Added spec for postgres adapter.
8
+
9
+ * Fixed Model.method_missing to work with new SQL generation.
10
+
11
+ * Fixed #compare_expr to support regexps.
12
+
13
+ * Fixed postgres, mysql adapters to support regexps.
14
+
15
+ * More specs for block filters. Updated README.
16
+
17
+ * Added support for globals and $X macros in block filters.
18
+
19
+ * Fixed Sequelizer to not fail if ParseTree or Ruby2Ruby gems are missing.
20
+
21
+ * Renamed String#expr into String#lit (#expr should be deprecated in future versions).
22
+
23
+ * Renamed Sequel::ExpressionString into LiteralString.
24
+
25
+ * Fixed Symbol#[] to return an ExpressionString, so as not to be literalized.
26
+
27
+ * Renamed Dataset::Expressions to Dataset::Sequelizer.
28
+
29
+ * Renamed Expressions#format_re_expression to match_expr.
30
+
31
+ * Renamed Expressions#format_eq_expression to compare_expr.
32
+
33
+ * Added support for Regexp in MySQL adapter.
34
+
35
+ * Refactored Regexp expressions into a separate #format_re_expression method.
36
+
37
+ * Added support for arithmetic in proc filters.
38
+
39
+ * Added support for nested proc expressions, more specs.
40
+
41
+ * Added support for SQL function using symbols, e.g. :sum[:x].
42
+
43
+ * Fixed deadlock bug in ConnectionPool.
44
+
45
+ * Removed deprecated old expressions.rb.
46
+
47
+ * Rewrote Proc filter feature using ParseTree.
48
+
49
+ * Added support for additional functions on columns using Symbol#method_missing.
50
+
51
+ * Added support for supplying filter block to DB#[] method, to allow stuff like DB[:nodes] {:path =~ /^icex1/}.
52
+
1
53
  === 0.1.9.12 (2007-08-26)
2
54
 
3
55
  * Added spec for PrettyTable.
data/README CHANGED
@@ -163,13 +163,13 @@ Or lists of values:
163
163
 
164
164
  my_posts = posts.filter(:category => ['ruby', 'postgres', 'linux'])
165
165
 
166
- Sequel now also accepts expressions as closures:
166
+ Sequel now also accepts expressions as closures, AKA block filters:
167
167
 
168
- my_posts = posts.filter {category == ['ruby', 'postgres', 'linux']}
168
+ my_posts = posts.filter {:category == ['ruby', 'postgres', 'linux']}
169
169
 
170
170
  Which also lets you do stuff like:
171
171
 
172
- my_posts = posts.filter {stamp > 1.month.ago}
172
+ my_posts = posts.filter {:stamp > 1.month.ago}
173
173
 
174
174
  Some adapters (like postgresql) will also let you specify Regexps:
175
175
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "sequel"
9
- VERS = "0.1.9.12"
9
+ VERS = "0.2.0"
10
10
  CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
11
11
  RDOC_OPTS = ['--quiet', '--title', "Sequel: Concise ORM for Ruby",
12
12
  "--opname", "index.html",
@@ -44,6 +44,8 @@ spec = Gem::Specification.new do |s|
44
44
  s.executables = ['sequel']
45
45
 
46
46
  s.add_dependency('metaid')
47
+ s.add_dependency('ParseTree')
48
+ s.add_dependency('ruby2ruby')
47
49
 
48
50
  s.required_ruby_version = '>= 1.8.4'
49
51
 
data/lib/sequel.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'metaid'
2
2
 
3
3
  files = %w[
4
- core_ext error connection_pool pretty_table expressions
4
+ core_ext error connection_pool pretty_table
5
5
  dataset migration model schema database
6
6
  ]
7
7
  dir = File.join(File.dirname(__FILE__), 'sequel')
@@ -94,7 +94,8 @@ module Sequel
94
94
  def make_new
95
95
  if @created_count < @max_size
96
96
  @created_count += 1
97
- @connection_proc.call
97
+ @connection_proc ? @connection_proc.call : \
98
+ (raise SequelError, "No connection proc specified")
98
99
  end
99
100
  end
100
101
 
@@ -18,10 +18,10 @@ class Array
18
18
  end
19
19
 
20
20
  module Sequel
21
- # ExpressionString is used to represent literal SQL expressions. An
22
- # ExpressionString is copied verbatim into an SQL statement. Instances of
23
- # ExpressionString can be created by calling String#expr.
24
- class ExpressionString < ::String
21
+ # LiteralString is used to represent literal SQL expressions. An
22
+ # LiteralString is copied verbatim into an SQL statement. Instances of
23
+ # LiteralString can be created by calling String#expr.
24
+ class LiteralString < ::String
25
25
  end
26
26
  end
27
27
 
@@ -39,19 +39,21 @@ class String
39
39
  to_sql.split(';').map {|s| s.strip}
40
40
  end
41
41
 
42
- # Converts a string into an ExpressionString, in order to override string
42
+ # Converts a string into an LiteralString, in order to override string
43
43
  # literalization, e.g.:
44
44
  #
45
45
  # DB[:items].filter(:abc => 'def').sql #=>
46
46
  # "SELECT * FROM items WHERE (abc = 'def')"
47
47
  #
48
- # DB[:items].filter(:abc => 'def'.expr).sql #=>
48
+ # DB[:items].filter(:abc => 'def'.lit).sql #=>
49
49
  # "SELECT * FROM items WHERE (abc = def)"
50
50
  #
51
- def expr
52
- Sequel::ExpressionString.new(self)
51
+ def lit
52
+ Sequel::LiteralString.new(self)
53
53
  end
54
54
 
55
+ alias_method :expr, :lit
56
+
55
57
  # Converts a string into a Time object.
56
58
  def to_time
57
59
  Time.parse(self)
@@ -93,18 +95,6 @@ module FieldCompositionMethods
93
95
  s
94
96
  end
95
97
  end
96
-
97
- # Formats a min(x) expression.
98
- def MIN; "min(#{to_field_name})"; end
99
-
100
- # Formats a max(x) expression.
101
- def MAX; "max(#{to_field_name})"; end
102
-
103
- # Formats a sum(x) expression.
104
- def SUM; "sum(#{to_field_name})"; end
105
-
106
- # Formats an avg(x) expression.
107
- def AVG; "avg(#{to_field_name})"; end
108
98
  end
109
99
 
110
100
  class String
@@ -139,6 +129,18 @@ class Symbol
139
129
  s
140
130
  end
141
131
  end
132
+
133
+ # Converts missing method calls into functions on columns, if the
134
+ # method name is made of all upper case letters.
135
+ def method_missing(sym)
136
+ ((s = sym.to_s) =~ /^([A-Z]+)$/) ? \
137
+ "#{s.downcase}(#{to_field_name})" : super
138
+ end
139
+
140
+ # Formats an SQL function with optional parameters
141
+ def [](*args)
142
+ "#{to_s}(#{args.join(', ')})".lit
143
+ end
142
144
  end
143
145
 
144
146
 
@@ -63,8 +63,12 @@ module Sequel
63
63
  Sequel::Dataset.new(self)
64
64
  end
65
65
 
66
- # Returns a new dataset with the from method invoked.
67
- def from(*args); dataset.from(*args); end
66
+ # Returns a new dataset with the from method invoked. If a block is given,
67
+ # it is used as a filter on the dataset.
68
+ def from(*args, &block)
69
+ ds = dataset.from(*args)
70
+ block ? ds.filter(&block) : ds
71
+ end
68
72
 
69
73
  # Returns a new dataset with the select method invoked.
70
74
  def select(*args); dataset.select(*args); end
@@ -1,8 +1,9 @@
1
1
  require 'time'
2
2
  require 'date'
3
3
 
4
- require File.join(File.dirname(__FILE__), 'dataset/dataset_sql')
5
- require File.join(File.dirname(__FILE__), 'dataset/dataset_convenience')
4
+ require File.join(File.dirname(__FILE__), 'dataset/sequelizer')
5
+ require File.join(File.dirname(__FILE__), 'dataset/sql')
6
+ require File.join(File.dirname(__FILE__), 'dataset/convenience')
6
7
 
7
8
  module Sequel
8
9
  # A Dataset represents a view of a the data in a database, constrained by
@@ -63,8 +64,9 @@ module Sequel
63
64
  # end
64
65
  class Dataset
65
66
  include Enumerable
66
- include SQL # in dataset/dataset_sql.rb
67
- include Convenience # in dataset/dataset_convenience.rb
67
+ include Sequelizer
68
+ include SQL
69
+ include Convenience
68
70
 
69
71
  attr_reader :db
70
72
  attr_accessor :opts
@@ -0,0 +1,246 @@
1
+ class Sequel::Dataset
2
+ # The Sequelizer module includes methods for translating Ruby expressions
3
+ # into SQL expressions, making it possible to specify dataset filters using
4
+ # blocks, e.g.:
5
+ #
6
+ # DB[:items].filter {:price < 100}
7
+ # DB[:items].filter {:category == 'ruby' && :date < 3.days.ago}
8
+ #
9
+ # Block filters can refer to literals, variables, constants, arguments,
10
+ # instance variables or anything else in order to create parameterized
11
+ # queries. Block filters can also refer to other dataset objects as
12
+ # sub-queries. Block filters are pretty much limitless!
13
+ #
14
+ # Block filters are based on ParseTree. If you do not have the ParseTree
15
+ # gem installed, block filters will raise an error.
16
+ #
17
+ # To enable full block filter support make sure you have both ParseTree and
18
+ # Ruby2Ruby installed:
19
+ #
20
+ # sudo gem install parsetree
21
+ # sudo gem install ruby2ruby
22
+ module Sequelizer
23
+ # Formats an comparison expression involving a left value and a right
24
+ # value. Comparison expressions differ according to the class of the right
25
+ # value. The stock implementation supports Range (inclusive and exclusive),
26
+ # Array (as a list of values to compare against), Dataset (as a subquery to
27
+ # compare against), or a regular value.
28
+ #
29
+ # dataset.compare_expr('id', 1..20) #=>
30
+ # "(id >= 1 AND id <= 20)"
31
+ # dataset.compare_expr('id', [3,6,10]) #=>
32
+ # "(id IN (3, 6, 10))"
33
+ # dataset.compare_expr('id', DB[:items].select(:id)) #=>
34
+ # "(id IN (SELECT id FROM items))"
35
+ # dataset.compare_expr('id', nil) #=>
36
+ # "(id IS NULL)"
37
+ # dataset.compare_expr('id', 3) #=>
38
+ # "(id = 3)"
39
+ def compare_expr(l, r)
40
+ case r
41
+ when Range:
42
+ r.exclude_end? ? \
43
+ "(#{l} >= #{literal(r.begin)} AND #{l} < #{literal(r.end)})" : \
44
+ "(#{l} >= #{literal(r.begin)} AND #{l} <= #{literal(r.end)})"
45
+ when Array:
46
+ "(#{literal(l)} IN (#{literal(r)}))"
47
+ when Sequel::Dataset:
48
+ "(#{literal(l)} IN (#{r.sql}))"
49
+ when NilClass:
50
+ "(#{literal(l)} IS NULL)"
51
+ when Regexp:
52
+ match_expr(l, r)
53
+ else
54
+ "(#{literal(l)} = #{literal(r)})"
55
+ end
56
+ end
57
+
58
+ # Formats a string matching expression. The stock implementation supports
59
+ # matching against strings only using the LIKE operator. Specific adapters
60
+ # can override this method to provide support for regular expressions.
61
+ def match_expr(l, r)
62
+ case r
63
+ when String:
64
+ "(#{literal(l)} LIKE #{literal(r)})"
65
+ else
66
+ raise SequelError, "Unsupported match pattern class (#{r.class})."
67
+ end
68
+ end
69
+
70
+ # Evaluates a method call. This method is used to evaluate Ruby expressions
71
+ # referring to indirect values, e.g.:
72
+ #
73
+ # dataset.filter {:category => category.to_s}
74
+ # dataset.filter {:x > y[0..3]}
75
+ #
76
+ # This method depends on the Ruby2Ruby gem. If you do not have Ruby2Ruby
77
+ # installed, this method will raise an error.
78
+ def ext_expr(e, b)
79
+ eval(RubyToRuby.new.process(e), b)
80
+ end
81
+
82
+ # Translates a method call parse-tree to SQL expression. The following
83
+ # operators are recognized and translated to SQL expressions: >, <, >=, <=,
84
+ # ==, =~, +, -, *, /, %:
85
+ #
86
+ # :x == 1 #=> "(x = 1)"
87
+ # (:x + 100) < 200 #=> "((x + 100) < 200)"
88
+ #
89
+ # The in, in?, nil and nil? method calls are intercepted and passed to
90
+ # #compare_expr.
91
+ #
92
+ # :x.in [1, 2, 3] #=> "(x IN (1, 2, 3))"
93
+ # :x.in?(DB[:y].select(:z)) #=> "(x IN (SELECT z FROM y))"
94
+ # :x.nil? #=> "(x IS NULL)"
95
+ #
96
+ # The like and like? method calls are intercepted and passed to #match_expr.
97
+ #
98
+ # :x.like? 'ABC%' #=> "(x LIKE 'ABC%')"
99
+ #
100
+ # The method also supports SQL functions by invoking Symbol#[]:
101
+ #
102
+ # :avg[:x] #=> "avg(x)"
103
+ # :substring[:x, 5] #=> "substring(x, 5)"
104
+ #
105
+ # All other method calls are evaulated as normal Ruby code.
106
+ def call_expr(e, b)
107
+ case op = e[2]
108
+ when :>, :<, :>=, :<=
109
+ l = pt_expr(e[1], b)
110
+ r = pt_expr(e[3][1], b)
111
+ "(#{l} #{op} #{r})"
112
+ when :==
113
+ l = eval_expr(e[1], b)
114
+ r = eval_expr(e[3][1], b)
115
+ compare_expr(l, r)
116
+ when :=~
117
+ l = eval_expr(e[1], b)
118
+ r = eval_expr(e[3][1], b)
119
+ match_expr(l, r)
120
+ when :+, :-, :*, :/, :%
121
+ l = pt_expr(e[1], b)
122
+ r = pt_expr(e[3][1], b)
123
+ "(#{l} #{op} #{r})"
124
+ when :in, :in?
125
+ # in/in? operators are supported using two forms:
126
+ # :x.in([1, 2, 3])
127
+ # :x.in(1, 2, 3) # variable arity
128
+ l = eval_expr(e[1], b)
129
+ r = eval_expr((e[3].size == 2) ? e[3][1] : e[3], b)
130
+ compare_expr(l, r)
131
+ when :nil, :nil?
132
+ l = eval_expr(e[1], b)
133
+ compare_expr(l, nil)
134
+ when :like, :like?
135
+ l = eval_expr(e[1], b)
136
+ r = eval_expr(e[3][1], b)
137
+ match_expr(l, r)
138
+ else
139
+ if (op == :[]) && (e[1][0] == :lit) && (Symbol === e[1][1])
140
+ # SQL Functions, e.g.: :sum[:x]
141
+ e[1][1][*pt_expr(e[3], b)]
142
+ else
143
+ # external code
144
+ ext_expr(e, b)
145
+ end
146
+ end
147
+ end
148
+
149
+ # Evaluates a parse-tree into an SQL expression.
150
+ def eval_expr(e, b)
151
+ case e[0]
152
+ when :call # method call
153
+ call_expr(e, b)
154
+ when :ivar, :cvar, :dvar, :vcall, :const, :gvar # local ref
155
+ eval(e[1].to_s, b)
156
+ when :nth_ref:
157
+ eval("$#{e[1]}", b)
158
+ when :lvar: # local context
159
+ if e[1] == :block
160
+ pr = eval(e[1].to_s, b)
161
+ "#{proc_to_sql(pr)}"
162
+ else
163
+ eval(e[1].to_s, b)
164
+ end
165
+ when :lit, :str # literal
166
+ e[1]
167
+ when :dot2 # inclusive range
168
+ eval_expr(e[1], b)..eval_expr(e[2], b)
169
+ when :dot3 # exclusive range
170
+ eval_expr(e[1], b)...eval_expr(e[2], b)
171
+ when :colon2 # qualified constant ref
172
+ eval_expr(e[1], b).const_get(e[2])
173
+ when :false: false
174
+ when :true: true
175
+ when :nil: nil
176
+ when :array
177
+ # array
178
+ e[1..-1].map {|i| eval_expr(i, b)}
179
+ when :match3
180
+ # =~/!~ operator
181
+ l = eval_expr(e[2], b)
182
+ r = eval_expr(e[1], b)
183
+ compare_expr(l, r)
184
+ when :iter
185
+ eval_expr(e[3], b)
186
+ when :dasgn, :dasgn_curr
187
+ # assignment
188
+ l = e[1]
189
+ r = eval_expr(e[2], b)
190
+ raise SequelError, "Invalid expression #{l} = #{r}. Did you mean :#{l} == #{r}?"
191
+ else
192
+ raise SequelError, "Invalid expression tree: #{e.inspect}"
193
+ end
194
+ end
195
+
196
+ def pt_expr(e, b)
197
+ case e[0]
198
+ when :not # negation: !x, (x != y), (x !~ y)
199
+ if (e[1][0] == :lit) && (Symbol === e[1][1])
200
+ # translate (!:x) into (x = 'f')
201
+ compare_expr(e[1][1], false)
202
+ else
203
+ "(NOT #{pt_expr(e[1], b)})"
204
+ end
205
+ when :block, :and # block of statements, x && y
206
+ "(#{e[1..-1].map {|i| pt_expr(i, b)}.join(" AND ")})"
207
+ when :or # x || y
208
+ "(#{pt_expr(e[1], b)} OR #{pt_expr(e[2], b)})"
209
+ when :call, :vcall, :iter # method calls, blocks
210
+ eval_expr(e, b)
211
+ else # literals
212
+ if e == [:lvar, :block]
213
+ eval_expr(e, b)
214
+ else
215
+ literal(eval_expr(e, b))
216
+ end
217
+ end
218
+ end
219
+
220
+ # Translates a Ruby block into an SQL expression.
221
+ def proc_to_sql(proc)
222
+ c = Class.new {define_method(:m, &proc)}
223
+ pt_expr(ParseTree.translate(c, :m)[2][2], proc.binding)
224
+ end
225
+ end
226
+ end
227
+
228
+ begin
229
+ require 'parse_tree'
230
+ rescue LoadError
231
+ module Sequel::Dataset::Sequelizer
232
+ def proc_to_sql(proc)
233
+ raise SequelError, "You must have the ParseTree gem installed in order to use block filters."
234
+ end
235
+ end
236
+ end
237
+
238
+ begin
239
+ require 'ruby2ruby'
240
+ rescue LoadError
241
+ module Sequel::Dataset::Sequelizer
242
+ def ext_expr(e)
243
+ raise SequelError, "You must have the Ruby2Ruby gem installed in order to use this block filter."
244
+ end
245
+ end
246
+ end