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 +52 -0
- data/README +3 -3
- data/Rakefile +3 -1
- data/lib/sequel.rb +1 -1
- data/lib/sequel/connection_pool.rb +2 -1
- data/lib/sequel/core_ext.rb +22 -20
- data/lib/sequel/database.rb +6 -2
- data/lib/sequel/dataset.rb +6 -4
- data/lib/sequel/dataset/{dataset_convenience.rb → convenience.rb} +0 -0
- data/lib/sequel/dataset/sequelizer.rb +246 -0
- data/lib/sequel/dataset/{dataset_sql.rb → sql.rb} +13 -81
- data/lib/sequel/model.rb +4 -4
- data/lib/sequel/mysql.rb +14 -2
- data/lib/sequel/postgres.rb +5 -7
- data/lib/sequel/schema/schema_generator.rb +1 -2
- data/spec/adapters/mysql_spec.rb +7 -0
- data/spec/adapters/postgres_spec.rb +87 -0
- data/spec/adapters/sqlite_spec.rb +2 -2
- data/spec/connection_pool_spec.rb +2 -2
- data/spec/core_ext_spec.rb +43 -2
- data/spec/database_spec.rb +6 -0
- data/spec/dataset_spec.rb +29 -30
- data/spec/sequelizer_spec.rb +250 -0
- metadata +48 -29
- data/spec/expressions_spec.rb +0 -151
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.
|
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
data/lib/sequel/core_ext.rb
CHANGED
@@ -18,10 +18,10 @@ class Array
|
|
18
18
|
end
|
19
19
|
|
20
20
|
module Sequel
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
class
|
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
|
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'.
|
48
|
+
# DB[:items].filter(:abc => 'def'.lit).sql #=>
|
49
49
|
# "SELECT * FROM items WHERE (abc = def)"
|
50
50
|
#
|
51
|
-
def
|
52
|
-
Sequel::
|
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
|
|
data/lib/sequel/database.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/sequel/dataset.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'time'
|
2
2
|
require 'date'
|
3
3
|
|
4
|
-
require File.join(File.dirname(__FILE__), 'dataset/
|
5
|
-
require File.join(File.dirname(__FILE__), 'dataset/
|
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
|
67
|
-
include
|
67
|
+
include Sequelizer
|
68
|
+
include SQL
|
69
|
+
include Convenience
|
68
70
|
|
69
71
|
attr_reader :db
|
70
72
|
attr_accessor :opts
|
File without changes
|
@@ -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
|