sequel 0.1.9.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG 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