sequel 0.2.0.2 → 0.2.1
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 +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
|