sequel 0.2.0.2 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +64 -0
- data/Rakefile +41 -1
- data/lib/sequel.rb +37 -37
- data/lib/sequel/core_ext.rb +3 -3
- data/lib/sequel/database.rb +8 -2
- data/lib/sequel/dataset.rb +103 -42
- data/lib/sequel/dataset/convenience.rb +46 -0
- data/lib/sequel/dataset/sequelizer.rb +34 -8
- data/lib/sequel/dataset/sql.rb +12 -8
- data/lib/sequel/model.rb +13 -237
- data/lib/sequel/model/base.rb +84 -0
- data/lib/sequel/model/hooks.rb +40 -0
- data/lib/sequel/model/record.rb +154 -0
- data/lib/sequel/model/relations.rb +26 -0
- data/lib/sequel/model/schema.rb +36 -0
- data/lib/sequel/mysql.rb +9 -7
- data/lib/sequel/postgres.rb +5 -4
- data/lib/sequel/schema/schema_generator.rb +1 -0
- data/lib/sequel/sqlite.rb +2 -2
- data/spec/core_ext_spec.rb +14 -0
- data/spec/database_spec.rb +12 -1
- data/spec/dataset_spec.rb +274 -0
- data/spec/model_spec.rb +286 -3
- data/spec/sequelizer_spec.rb +49 -9
- data/spec/spec_helper.rb +27 -5
- metadata +8 -2
@@ -3,6 +3,10 @@ require 'enumerator'
|
|
3
3
|
module Sequel
|
4
4
|
class Dataset
|
5
5
|
module Convenience
|
6
|
+
def empty?
|
7
|
+
count == 0
|
8
|
+
end
|
9
|
+
|
6
10
|
# Returns the first record in the dataset.
|
7
11
|
def single_record(opts = nil)
|
8
12
|
each(opts) {|r| return r}
|
@@ -186,6 +190,48 @@ module Sequel
|
|
186
190
|
end
|
187
191
|
end
|
188
192
|
end
|
193
|
+
|
194
|
+
module QueryBlockCopy
|
195
|
+
def each(*args); raise SequelError, "#each cannot be invoked inside a query block."; end
|
196
|
+
def insert(*args); raise SequelError, "#insert cannot be invoked inside a query block."; end
|
197
|
+
def update(*args); raise SequelError, "#update cannot be invoked inside a query block."; end
|
198
|
+
def delete(*args); raise SequelError, "#delete cannot be invoked inside a query block."; end
|
199
|
+
|
200
|
+
def clone_merge(opts)
|
201
|
+
@opts.merge!(opts)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Translates a query block into a dataset. Query blocks can be useful
|
206
|
+
# when expressing complex SELECT statements, e.g.:
|
207
|
+
#
|
208
|
+
# dataset = DB[:items].query do
|
209
|
+
# select :x, :y, :z
|
210
|
+
# where {:x > 1 && :y > 2}
|
211
|
+
# order_by :z.DESC
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
def query(&block)
|
215
|
+
copy = clone_merge({})
|
216
|
+
copy.extend(QueryBlockCopy)
|
217
|
+
copy.instance_eval(&block)
|
218
|
+
clone_merge(copy.opts)
|
219
|
+
end
|
220
|
+
|
221
|
+
MUTATION_RE = /^(.+)!$/.freeze
|
222
|
+
|
223
|
+
def method_missing(m, *args, &block)
|
224
|
+
if m.to_s =~ MUTATION_RE
|
225
|
+
m = $1.to_sym
|
226
|
+
super unless respond_to?(m)
|
227
|
+
copy = send(m, *args, &block)
|
228
|
+
super if copy.class != self.class
|
229
|
+
@opts.merge!(copy.opts)
|
230
|
+
self
|
231
|
+
else
|
232
|
+
super
|
233
|
+
end
|
234
|
+
end
|
189
235
|
end
|
190
236
|
end
|
191
237
|
end
|
@@ -108,7 +108,11 @@ class Sequel::Dataset
|
|
108
108
|
when :>, :<, :>=, :<=
|
109
109
|
l = eval_expr(e[1], b)
|
110
110
|
r = eval_expr(e[3][1], b)
|
111
|
-
|
111
|
+
if (Symbol === l) || (Sequel::LiteralString === l) || (Symbol === r) || (Sequel::LiteralString === r)
|
112
|
+
"(#{literal(l)} #{op} #{literal(r)})"
|
113
|
+
else
|
114
|
+
ext_expr(e, b)
|
115
|
+
end
|
112
116
|
when :==
|
113
117
|
l = eval_expr(e[1], b)
|
114
118
|
r = eval_expr(e[3][1], b)
|
@@ -120,7 +124,11 @@ class Sequel::Dataset
|
|
120
124
|
when :+, :-, :*, :/, :%
|
121
125
|
l = eval_expr(e[1], b)
|
122
126
|
r = eval_expr(e[3][1], b)
|
123
|
-
|
127
|
+
if (Symbol === l) || (Sequel::LiteralString === l) || (Symbol === r) || (Sequel::LiteralString === r)
|
128
|
+
"(#{literal(l)} #{op} #{literal(r)})".lit
|
129
|
+
else
|
130
|
+
ext_expr(e, b)
|
131
|
+
end
|
124
132
|
when :in, :in?
|
125
133
|
# in/in? operators are supported using two forms:
|
126
134
|
# :x.in([1, 2, 3])
|
@@ -146,12 +154,32 @@ class Sequel::Dataset
|
|
146
154
|
end
|
147
155
|
end
|
148
156
|
|
157
|
+
def fcall_expr(e, b)
|
158
|
+
ext_expr(e, b)
|
159
|
+
end
|
160
|
+
|
161
|
+
def vcall_expr(e, b)
|
162
|
+
eval(e[1].to_s, b)
|
163
|
+
end
|
164
|
+
|
165
|
+
def iter_expr(e, b)
|
166
|
+
if e[1] == [:fcall, :proc]
|
167
|
+
eval_expr(e[3], b) # inline proc
|
168
|
+
else
|
169
|
+
ext_expr(e, b) # method call with inline proc
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
149
173
|
# Evaluates a parse-tree into an SQL expression.
|
150
174
|
def eval_expr(e, b)
|
151
175
|
case e[0]
|
152
176
|
when :call # method call
|
153
177
|
call_expr(e, b)
|
154
|
-
when :
|
178
|
+
when :fcall
|
179
|
+
fcall_expr(e, b)
|
180
|
+
when :vcall
|
181
|
+
vcall_expr(e, b)
|
182
|
+
when :ivar, :cvar, :dvar, :const, :gvar # local ref
|
155
183
|
eval(e[1].to_s, b)
|
156
184
|
when :nth_ref:
|
157
185
|
eval("$#{e[1]}", b)
|
@@ -182,16 +210,14 @@ class Sequel::Dataset
|
|
182
210
|
r = eval_expr(e[1], b)
|
183
211
|
compare_expr(l, r)
|
184
212
|
when :iter
|
185
|
-
|
186
|
-
eval_expr(e[3], b) # inline proc
|
187
|
-
else
|
188
|
-
ext_expr(e, b) # method call with inline proc
|
189
|
-
end
|
213
|
+
iter_expr(e, b)
|
190
214
|
when :dasgn, :dasgn_curr
|
191
215
|
# assignment
|
192
216
|
l = e[1]
|
193
217
|
r = eval_expr(e[2], b)
|
194
218
|
raise SequelError, "Invalid expression #{l} = #{r}. Did you mean :#{l} == #{r}?"
|
219
|
+
when :if, :dstr
|
220
|
+
ext_expr(e, b)
|
195
221
|
else
|
196
222
|
raise SequelError, "Invalid expression tree: #{e.inspect}"
|
197
223
|
end
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -145,6 +145,8 @@ module Sequel
|
|
145
145
|
def order(*order)
|
146
146
|
clone_merge(:order => order)
|
147
147
|
end
|
148
|
+
|
149
|
+
alias_method :order_by, :order
|
148
150
|
|
149
151
|
# Returns a copy of the dataset with the order reversed. If no order is
|
150
152
|
# given, the existing order is inverted.
|
@@ -176,6 +178,8 @@ module Sequel
|
|
176
178
|
def group(*fields)
|
177
179
|
clone_merge(:group => fields)
|
178
180
|
end
|
181
|
+
|
182
|
+
alias_method :group_by, :group
|
179
183
|
|
180
184
|
# Returns a copy of the dataset with the given conditions imposed upon it.
|
181
185
|
# If the query has been grouped, then the conditions are imposed in the
|
@@ -424,15 +428,14 @@ module Sequel
|
|
424
428
|
values = values[0] if values.size == 1
|
425
429
|
case values
|
426
430
|
when Hash
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
431
|
+
values = transform_save(values) if @transform
|
432
|
+
if values.empty?
|
433
|
+
"INSERT INTO #{@opts[:from]} DEFAULT VALUES;"
|
434
|
+
else
|
435
|
+
fl, vl = [], []
|
436
|
+
values.each {|k, v| fl << field_name(k); vl << literal(v)}
|
437
|
+
"INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)});"
|
432
438
|
end
|
433
|
-
fl = field_list.join(COMMA_SEPARATOR)
|
434
|
-
vl = value_list.join(COMMA_SEPARATOR)
|
435
|
-
"INSERT INTO #{@opts[:from]} (#{fl}) VALUES (#{vl});"
|
436
439
|
when Dataset
|
437
440
|
"INSERT INTO #{@opts[:from]} #{literal(values)}"
|
438
441
|
else
|
@@ -454,6 +457,7 @@ module Sequel
|
|
454
457
|
raise SequelError, "Can't update a joined dataset"
|
455
458
|
end
|
456
459
|
|
460
|
+
values = transform_save(values) if @transform
|
457
461
|
set_list = values.map {|k, v| "#{field_name(k)} = #{literal(v)}"}.
|
458
462
|
join(COMMA_SEPARATOR)
|
459
463
|
sql = "UPDATE #{@opts[:from]} SET #{set_list}"
|
data/lib/sequel/model.rb
CHANGED
@@ -1,161 +1,27 @@
|
|
1
1
|
module Sequel
|
2
2
|
class Model
|
3
|
-
|
4
|
-
|
5
|
-
def self.db
|
6
|
-
@db ||= ((superclass != Object) && (superclass.db)) || nil
|
7
|
-
end
|
8
|
-
def self.db=(db); @db = db; end
|
9
|
-
def self.database_opened(db)
|
10
|
-
@db = db if self == Model && !@db
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.table_name
|
14
|
-
@table_name ||= ((superclass != Model) && (superclass.table_name)) || nil
|
15
|
-
end
|
16
|
-
def self.set_table_name(t); @table_name = t; end
|
3
|
+
end
|
4
|
+
end
|
17
5
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
raise SequelError, "Database not specified for #{self}."
|
24
|
-
end
|
25
|
-
@dataset = db[table_name]
|
26
|
-
@dataset.set_model(self)
|
27
|
-
@dataset
|
28
|
-
end
|
6
|
+
require File.join(File.dirname(__FILE__), 'model/base')
|
7
|
+
require File.join(File.dirname(__FILE__), 'model/hooks')
|
8
|
+
require File.join(File.dirname(__FILE__), 'model/record')
|
9
|
+
require File.join(File.dirname(__FILE__), 'model/schema')
|
10
|
+
require File.join(File.dirname(__FILE__), 'model/relations')
|
29
11
|
|
30
|
-
|
31
|
-
|
32
|
-
@dataset = ds
|
33
|
-
@dataset.set_model(self)
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.cache_by(column, expiration)
|
37
|
-
@cache_column = column
|
38
|
-
|
39
|
-
prefix = "#{name}.#{column}."
|
40
|
-
define_method(:cache_key) do
|
41
|
-
prefix + @values[column].to_s
|
42
|
-
end
|
43
|
-
|
44
|
-
define_method("find_by_#{column}".to_sym) do |arg|
|
45
|
-
key = cache_key
|
46
|
-
rec = CACHE[key]
|
47
|
-
if !rec
|
48
|
-
rec = find(column => arg)
|
49
|
-
CACHE.set(key, rec, expiration)
|
50
|
-
end
|
51
|
-
rec
|
52
|
-
end
|
53
|
-
|
54
|
-
alias_method :destroy, :destroy_and_invalidate_cache
|
55
|
-
alias_method :set, :set_and_update_cache
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.cache_column
|
59
|
-
@cache_column
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.primary_key; @primary_key ||= :id; end
|
63
|
-
def self.set_primary_key(k); @primary_key = k; end
|
64
|
-
|
65
|
-
def self.set_schema(name = nil, &block)
|
66
|
-
name ? set_table_name(name) : name = table_name
|
67
|
-
@schema = Schema::Generator.new(db, name, &block)
|
68
|
-
if @schema.primary_key_name
|
69
|
-
set_primary_key @schema.primary_key_name
|
70
|
-
end
|
71
|
-
end
|
72
|
-
def self.schema
|
73
|
-
@schema || ((superclass != Model) && (superclass.schema))
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.table_exists?
|
77
|
-
db.table_exists?(table_name)
|
78
|
-
end
|
79
|
-
|
80
|
-
def self.create_table
|
81
|
-
db.create_table_sql_list(*schema.create_info).each {|s| db << s}
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.drop_table
|
85
|
-
db.execute db.drop_table_sql(table_name)
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.recreate_table
|
89
|
-
drop_table if table_exists?
|
90
|
-
create_table
|
91
|
-
end
|
92
|
-
|
12
|
+
module Sequel
|
13
|
+
class Model
|
93
14
|
def self.subset(name, *args, &block)
|
94
15
|
meta_def(name) {filter(*args, &block)}
|
95
16
|
end
|
96
17
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
def self.one_to_one(name, opts)
|
102
|
-
klass = opts[:class] ? opts[:class] : (FROM_DATASET % name.inspect)
|
103
|
-
key = opts[:key] || (name.to_s + ID_POSTFIX)
|
104
|
-
define_method name, &eval(ONE_TO_ONE_PROC % [key, klass])
|
105
|
-
end
|
106
|
-
|
107
|
-
ONE_TO_MANY_PROC = "proc {%s.filter(:%s => @pkey)}".freeze
|
108
|
-
ONE_TO_MANY_ORDER_PROC = "proc {%s.filter(:%s => @pkey).order(%s)}".freeze
|
109
|
-
def self.one_to_many(name, opts)
|
110
|
-
klass = opts[:class] ? opts[:class] :
|
111
|
-
(FROM_DATASET % (opts[:table] || name.inspect))
|
112
|
-
key = opts[:on]
|
113
|
-
order = opts[:order]
|
114
|
-
define_method name, &eval(
|
115
|
-
(order ? ONE_TO_MANY_ORDER_PROC : ONE_TO_MANY_PROC) %
|
116
|
-
[klass, key, order.inspect]
|
117
|
-
)
|
118
|
-
end
|
119
|
-
|
120
|
-
def self.get_hooks(key)
|
121
|
-
@hooks ||= {}
|
122
|
-
@hooks[key] ||= []
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.has_hooks?(key)
|
126
|
-
!get_hooks(key).empty?
|
127
|
-
end
|
128
|
-
|
129
|
-
def run_hooks(key)
|
130
|
-
self.class.get_hooks(key).each {|h| instance_eval(&h)}
|
131
|
-
end
|
132
|
-
|
133
|
-
def self.before_save(&block)
|
134
|
-
get_hooks(:before_save).unshift(block)
|
135
|
-
end
|
136
|
-
|
137
|
-
def self.before_create(&block)
|
138
|
-
get_hooks(:before_create).unshift(block)
|
139
|
-
end
|
140
|
-
|
141
|
-
def self.before_destroy(&block)
|
142
|
-
get_hooks(:before_destroy).unshift(block)
|
143
|
-
end
|
144
|
-
|
145
|
-
def self.after_save(&block)
|
146
|
-
get_hooks(:after_save) << block
|
147
|
-
end
|
148
|
-
|
149
|
-
def self.after_create(&block)
|
150
|
-
get_hooks(:after_create) << block
|
151
|
-
end
|
152
|
-
|
153
|
-
def self.after_destroy(&block)
|
154
|
-
get_hooks(:after_destroy) << block
|
18
|
+
def primary_key_hash(value)
|
19
|
+
# stock implementation
|
20
|
+
{:id => value}
|
155
21
|
end
|
156
22
|
|
157
23
|
def self.find(cond)
|
158
|
-
dataset[cond.is_a?(Hash) ? cond :
|
24
|
+
dataset[cond.is_a?(Hash) ? cond : primary_key_hash(cond)]
|
159
25
|
end
|
160
26
|
|
161
27
|
def self.find_or_create(cond)
|
@@ -166,56 +32,11 @@ module Sequel
|
|
166
32
|
|
167
33
|
############################################################################
|
168
34
|
|
169
|
-
attr_reader :values, :pkey
|
170
|
-
|
171
|
-
def model
|
172
|
-
self.class
|
173
|
-
end
|
174
|
-
|
175
|
-
def primary_key
|
176
|
-
self.class.primary_key
|
177
|
-
end
|
178
|
-
|
179
|
-
def initialize(values)
|
180
|
-
@values = values
|
181
|
-
@pkey = values[self.class.primary_key]
|
182
|
-
end
|
183
|
-
|
184
|
-
def exists?
|
185
|
-
model.filter(primary_key => @pkey).count == 1
|
186
|
-
end
|
187
|
-
|
188
|
-
def refresh
|
189
|
-
@values = self.class.dataset.naked[primary_key => @pkey] ||
|
190
|
-
(raise SequelError, "Record not found")
|
191
|
-
self
|
192
|
-
end
|
193
|
-
|
194
35
|
def self.destroy_all
|
195
36
|
has_hooks?(:before_destroy) ? dataset.destroy : dataset.delete
|
196
37
|
end
|
197
38
|
def self.delete_all; dataset.delete; end
|
198
39
|
|
199
|
-
def self.create(values = nil)
|
200
|
-
db.transaction do
|
201
|
-
obj = new(values || {})
|
202
|
-
obj.save
|
203
|
-
obj
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def destroy
|
208
|
-
db.transaction do
|
209
|
-
run_hooks(:before_destroy)
|
210
|
-
delete
|
211
|
-
run_hooks(:after_destroy)
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def delete
|
216
|
-
model.dataset.filter(primary_key => @pkey).delete
|
217
|
-
end
|
218
|
-
|
219
40
|
FIND_BY_REGEXP = /^find_by_(.*)/.freeze
|
220
41
|
FILTER_BY_REGEXP = /^filter_by_(.*)/.freeze
|
221
42
|
ALL_BY_REGEXP = /^all_by_(.*)/.freeze
|
@@ -244,63 +65,18 @@ module Sequel
|
|
244
65
|
dataset.join(*args).select(table_name.to_sym.ALL)
|
245
66
|
end
|
246
67
|
|
247
|
-
def db; self.class.db; end
|
248
|
-
|
249
68
|
def [](field); @values[field]; end
|
250
69
|
|
251
70
|
def []=(field, value); @values[field] = value; end
|
252
71
|
|
253
|
-
WRITE_ATTR_REGEXP = /(.*)=$/.freeze
|
254
|
-
|
255
|
-
def method_missing(m, value = nil)
|
256
|
-
write = m.to_s =~ WRITE_ATTR_REGEXP
|
257
|
-
att = write ? $1.to_sym : m
|
258
|
-
# raise unless the att is recognized or this is a new unaved record
|
259
|
-
super unless @values.include?(att) || !@pkey
|
260
|
-
|
261
|
-
write ? (self[att] = value) : self[att]
|
262
|
-
end
|
263
|
-
|
264
72
|
def each(&block); @values.each(&block); end
|
265
73
|
def keys; @values.keys; end
|
266
74
|
|
267
75
|
def id; @values[:id]; end
|
268
76
|
|
269
|
-
def save
|
270
|
-
run_hooks(:before_save)
|
271
|
-
if @pkey
|
272
|
-
run_hooks(:before_update)
|
273
|
-
model.dataset.filter(primary_key => @pkey).update(@values)
|
274
|
-
run_hooks(:after_update)
|
275
|
-
else
|
276
|
-
run_hooks(:before_create)
|
277
|
-
@pkey = model.dataset.insert(@values)
|
278
|
-
refresh
|
279
|
-
run_hooks(:after_create)
|
280
|
-
end
|
281
|
-
run_hooks(:after_save)
|
282
|
-
end
|
283
|
-
|
284
77
|
def ==(obj)
|
285
78
|
(obj.class == model) && (obj.values == @values)
|
286
79
|
end
|
287
|
-
|
288
|
-
def set(values)
|
289
|
-
model.dataset.filter(primary_key => @pkey).update(values)
|
290
|
-
@values.merge!(values)
|
291
|
-
end
|
292
80
|
end
|
293
81
|
|
294
|
-
def self.Model(table)
|
295
|
-
@models ||= {}
|
296
|
-
@models[table] ||= Class.new(Sequel::Model) do
|
297
|
-
meta_def(:inherited) do |c|
|
298
|
-
if table.is_a?(Dataset)
|
299
|
-
c.set_dataset(table)
|
300
|
-
else
|
301
|
-
c.set_table_name(table)
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
82
|
end
|