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
@@ -0,0 +1,84 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
# returns the database associated with the model class
|
4
|
+
def self.db
|
5
|
+
@db ||= ((superclass != Object) && (superclass.db)) || \
|
6
|
+
raise(SequelError, "No database associated with #{self}.")
|
7
|
+
end
|
8
|
+
|
9
|
+
# sets the database associated with the model class
|
10
|
+
def self.db=(db); @db = db; end
|
11
|
+
|
12
|
+
# called when a database is opened in order to automatically associate the
|
13
|
+
# first opened database with model classes.
|
14
|
+
def self.database_opened(db)
|
15
|
+
@db = db if self == Model && !@db
|
16
|
+
end
|
17
|
+
|
18
|
+
# returns the dataset associated with the model class.
|
19
|
+
def self.dataset
|
20
|
+
@dataset || super_dataset || raise(SequelError, "No dataset associated with #{self}.")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.super_dataset
|
24
|
+
if superclass && superclass.respond_to?(:dataset) && ds = superclass.dataset
|
25
|
+
ds
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.columns
|
30
|
+
@columns ||= @dataset.columns || raise(SequelError, "Could not fetch columns for #{self}")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets the dataset associated with the model class.
|
34
|
+
def self.set_dataset(ds)
|
35
|
+
@db = ds.db
|
36
|
+
@dataset = ds
|
37
|
+
@dataset.set_model(self)
|
38
|
+
@dataset.transform(@transform) if @transform
|
39
|
+
end
|
40
|
+
|
41
|
+
def model
|
42
|
+
@model ||= self.class
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the dataset assoiated with the object's model class.
|
46
|
+
def db
|
47
|
+
@db ||= model.db
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the dataset assoiated with the object's model class.
|
51
|
+
def dataset
|
52
|
+
@dataset ||= model.dataset
|
53
|
+
end
|
54
|
+
|
55
|
+
def columns
|
56
|
+
@columns ||= model.columns
|
57
|
+
end
|
58
|
+
|
59
|
+
SERIALIZE_FORMATS = {
|
60
|
+
:yaml => [proc {|v| YAML.load v if v}, proc {|v| v.to_yaml}],
|
61
|
+
:marshal => [proc {|v| Marshal.load(v) if v}, proc {|v| Marshal.dump(v)}]
|
62
|
+
}
|
63
|
+
|
64
|
+
def self.serialize(*columns)
|
65
|
+
format = columns.pop[:format] if Hash === columns.last
|
66
|
+
filters = SERIALIZE_FORMATS[format || :yaml]
|
67
|
+
# add error handling here
|
68
|
+
|
69
|
+
@transform = columns.inject({}) do |m, c|
|
70
|
+
m[c] = filters
|
71
|
+
m
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.Model(source)
|
77
|
+
@models ||= {}
|
78
|
+
@models[source] ||= Class.new(Sequel::Model) do
|
79
|
+
meta_def(:inherited) do |c|
|
80
|
+
c.set_dataset(source.is_a?(Dataset) ? source : c.db[source])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
def self.get_hooks(key)
|
4
|
+
@hooks ||= {}
|
5
|
+
@hooks[key] ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.has_hooks?(key)
|
9
|
+
!get_hooks(key).empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_hooks(key)
|
13
|
+
self.class.get_hooks(key).each {|h| instance_eval(&h)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.before_save(&block)
|
17
|
+
get_hooks(:before_save).unshift(block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.before_create(&block)
|
21
|
+
get_hooks(:before_create).unshift(block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.before_destroy(&block)
|
25
|
+
get_hooks(:before_destroy).unshift(block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.after_save(&block)
|
29
|
+
get_hooks(:after_save) << block
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.after_create(&block)
|
33
|
+
get_hooks(:after_create) << block
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.after_destroy(&block)
|
37
|
+
get_hooks(:after_destroy) << block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
attr_reader :values
|
4
|
+
|
5
|
+
def self.primary_key; :id; end
|
6
|
+
def self.primary_key_hash(v); {:id => v}; end
|
7
|
+
|
8
|
+
def self.set_primary_key(key)
|
9
|
+
# if k is nil, we go to no_primary_key
|
10
|
+
return no_primary_key unless key
|
11
|
+
|
12
|
+
# redefine primary_key
|
13
|
+
meta_def(:primary_key) {key}
|
14
|
+
|
15
|
+
if key.is_a?(Array) # composite key
|
16
|
+
class_def(:this) do
|
17
|
+
@this ||= self.class.dataset.filter( \
|
18
|
+
@values.reject {|k, v| !key.include?(k)} \
|
19
|
+
).naked
|
20
|
+
end
|
21
|
+
meta_def(:primary_key_hash) do |v|
|
22
|
+
key.inject({}) {|m, i| m[i] = v.shift; m}
|
23
|
+
end
|
24
|
+
else # regular key
|
25
|
+
class_def(:this) do
|
26
|
+
@this ||= self.class.dataset.filter(key => @values[key]).naked
|
27
|
+
end
|
28
|
+
meta_def(:primary_key_hash) do |v|
|
29
|
+
{key => v}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.no_primary_key
|
35
|
+
meta_def(:primary_key) {nil}
|
36
|
+
meta_def(:primary_key_hash) {|v| raise SequelError, "#{self} does not have a primary key"}
|
37
|
+
class_def(:this) {raise SequelError, "No primary key is associated with this model"}
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.create(values = {})
|
41
|
+
db.transaction do
|
42
|
+
obj = new(values, true)
|
43
|
+
obj.save
|
44
|
+
obj
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def this
|
49
|
+
@this ||= self.class.dataset.filter(:id => @values[:id]).naked
|
50
|
+
end
|
51
|
+
|
52
|
+
# instance method
|
53
|
+
def primary_key
|
54
|
+
@primary_key ||= self.class.primary_key
|
55
|
+
end
|
56
|
+
|
57
|
+
def pkey
|
58
|
+
@pkey ||= @values[primary_key]
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(values = {}, new = false)
|
62
|
+
@values = values
|
63
|
+
@new = new
|
64
|
+
if !new # determine if it's a new record
|
65
|
+
pk = primary_key
|
66
|
+
@new = (pk == nil) || (!(Array === pk) && !@values[pk])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def new?
|
71
|
+
@new
|
72
|
+
end
|
73
|
+
|
74
|
+
def exists?
|
75
|
+
this.count > 0
|
76
|
+
end
|
77
|
+
|
78
|
+
def save
|
79
|
+
run_hooks(:before_save)
|
80
|
+
if @new
|
81
|
+
run_hooks(:before_create)
|
82
|
+
iid = model.dataset.insert(@values)
|
83
|
+
# if we have a regular primary key and it's not set in @values,
|
84
|
+
# we assume it's the last inserted id
|
85
|
+
if (pk = primary_key) && !(Array === pk) && !@values[pk]
|
86
|
+
@values[pk] = iid
|
87
|
+
end
|
88
|
+
if pk
|
89
|
+
@this = nil # remove memoized this dataset
|
90
|
+
refresh
|
91
|
+
end
|
92
|
+
run_hooks(:after_create)
|
93
|
+
else
|
94
|
+
run_hooks(:before_update)
|
95
|
+
this.update(@values)
|
96
|
+
run_hooks(:after_update)
|
97
|
+
end
|
98
|
+
run_hooks(:after_save)
|
99
|
+
@new = false
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
def set(values)
|
104
|
+
this.update(values)
|
105
|
+
@values.merge!(values)
|
106
|
+
end
|
107
|
+
|
108
|
+
def refresh
|
109
|
+
@values = this.first || raise(SequelError, "Record not found")
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
def destroy
|
114
|
+
db.transaction do
|
115
|
+
run_hooks(:before_destroy)
|
116
|
+
delete
|
117
|
+
run_hooks(:after_destroy)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def delete
|
122
|
+
this.delete
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
ATTR_RE = /^([a-zA-Z_]\w*)(=)?$/.freeze
|
127
|
+
|
128
|
+
def method_missing(m, *args)
|
129
|
+
if m.to_s =~ ATTR_RE
|
130
|
+
att = $1.to_sym
|
131
|
+
write = $2 == '='
|
132
|
+
|
133
|
+
# check wether the column is legal
|
134
|
+
unless columns.include?(att)
|
135
|
+
raise SequelError, "Invalid column (#{att.inspect}) for #{self}"
|
136
|
+
end
|
137
|
+
|
138
|
+
# define the column accessor
|
139
|
+
Thread.exclusive do
|
140
|
+
if write
|
141
|
+
model.class_def(m) {|v| @values[att] = v}
|
142
|
+
else
|
143
|
+
model.class_def(m) {@values[att]}
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# call the accessor
|
148
|
+
respond_to?(m) ? send(m, *args) : super(m, *args)
|
149
|
+
else
|
150
|
+
super(m, *args)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
ONE_TO_ONE_PROC = "proc {i = @values[:%s]; %s[i] if i}".freeze
|
4
|
+
ID_POSTFIX = "_id".freeze
|
5
|
+
FROM_DATASET = "db[%s]".freeze
|
6
|
+
|
7
|
+
def self.one_to_one(name, opts)
|
8
|
+
klass = opts[:class] ? opts[:class] : (FROM_DATASET % name.inspect)
|
9
|
+
key = opts[:key] || (name.to_s + ID_POSTFIX)
|
10
|
+
define_method name, &eval(ONE_TO_ONE_PROC % [key, klass])
|
11
|
+
end
|
12
|
+
|
13
|
+
ONE_TO_MANY_PROC = "proc {%s.filter(:%s => pkey)}".freeze
|
14
|
+
ONE_TO_MANY_ORDER_PROC = "proc {%s.filter(:%s => pkey).order(%s)}".freeze
|
15
|
+
def self.one_to_many(name, opts)
|
16
|
+
klass = opts[:class] ? opts[:class] :
|
17
|
+
(FROM_DATASET % (opts[:table] || name.inspect))
|
18
|
+
key = opts[:on]
|
19
|
+
order = opts[:order]
|
20
|
+
define_method name, &eval(
|
21
|
+
(order ? ONE_TO_MANY_ORDER_PROC : ONE_TO_MANY_PROC) %
|
22
|
+
[klass, key, order.inspect]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
def self.set_schema(name = nil, &block)
|
4
|
+
name ? set_dataset(db[name]) : name = table_name
|
5
|
+
@schema = Schema::Generator.new(db, name, &block)
|
6
|
+
if @schema.primary_key_name
|
7
|
+
set_primary_key @schema.primary_key_name
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.schema
|
12
|
+
@schema || ((superclass != Model) && (superclass.schema))
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.table_name
|
16
|
+
dataset.opts[:from].first
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.table_exists?
|
20
|
+
db.table_exists?(table_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.create_table
|
24
|
+
db.create_table_sql_list(*schema.create_info).each {|s| db << s}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.drop_table
|
28
|
+
db.execute db.drop_table_sql(table_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.recreate_table
|
32
|
+
drop_table if table_exists?
|
33
|
+
create_table
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/sequel/mysql.rb
CHANGED
@@ -69,10 +69,10 @@ module Sequel
|
|
69
69
|
conn = Mysql.real_connect(@opts[:host], @opts[:user], @opts[:password],
|
70
70
|
@opts[:database], @opts[:port])
|
71
71
|
conn.query_with_result = false
|
72
|
-
if @opts[:charset]
|
73
|
-
conn.query("set character_set_connection = '#{
|
74
|
-
conn.query("set character_set_client = '#{
|
75
|
-
conn.query("set character_set_results = '#{
|
72
|
+
if encoding = @opts[:encoding] || @opts[:charset]
|
73
|
+
conn.query("set character_set_connection = '#{encoding}';")
|
74
|
+
conn.query("set character_set_client = '#{encoding}';")
|
75
|
+
conn.query("set character_set_results = '#{encoding}';")
|
76
76
|
end
|
77
77
|
conn.reconnect = true
|
78
78
|
conn
|
@@ -95,7 +95,7 @@ module Sequel
|
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
|
-
def
|
98
|
+
def execute_select(sql)
|
99
99
|
@logger.info(sql) if @logger
|
100
100
|
@pool.hold do |conn|
|
101
101
|
conn.query(sql)
|
@@ -176,7 +176,9 @@ module Sequel
|
|
176
176
|
def match_expr(l, r)
|
177
177
|
case r
|
178
178
|
when Regexp:
|
179
|
-
|
179
|
+
r.casefold? ? \
|
180
|
+
"(#{literal(l)} REGEXP #{literal(r.source)})" :
|
181
|
+
"(#{literal(l)} REGEXP BINARY #{literal(r.source)})"
|
180
182
|
else
|
181
183
|
super
|
182
184
|
end
|
@@ -213,7 +215,7 @@ module Sequel
|
|
213
215
|
|
214
216
|
def fetch_rows(sql)
|
215
217
|
@db.synchronize do
|
216
|
-
r = @db.
|
218
|
+
r = @db.execute_select(sql)
|
217
219
|
begin
|
218
220
|
@columns = r.columns
|
219
221
|
r.each_hash {|row| yield row}
|
data/lib/sequel/postgres.rb
CHANGED
@@ -141,7 +141,7 @@ module Sequel
|
|
141
141
|
set_adapter_scheme :postgres
|
142
142
|
|
143
143
|
def connect
|
144
|
-
PGconn.connect(
|
144
|
+
conn = PGconn.connect(
|
145
145
|
@opts[:host] || 'localhost',
|
146
146
|
@opts[:port] || 5432,
|
147
147
|
'', '',
|
@@ -149,6 +149,10 @@ module Sequel
|
|
149
149
|
@opts[:user],
|
150
150
|
@opts[:password]
|
151
151
|
)
|
152
|
+
if encoding = @opts[:encoding] || @opts[:charset]
|
153
|
+
conn.set_client_encoding(encoding)
|
154
|
+
end
|
155
|
+
conn
|
152
156
|
end
|
153
157
|
|
154
158
|
def dataset(opts = nil)
|
@@ -282,9 +286,6 @@ module Sequel
|
|
282
286
|
end
|
283
287
|
end
|
284
288
|
|
285
|
-
LIKE = '(%s ~ %s)'.freeze
|
286
|
-
LIKE_CI = '%s ~* %s'.freeze
|
287
|
-
|
288
289
|
def match_expr(l, r)
|
289
290
|
case r
|
290
291
|
when Regexp:
|
data/lib/sequel/sqlite.rb
CHANGED
@@ -47,7 +47,7 @@ module Sequel
|
|
47
47
|
@pool.hold {|conn| conn.get_first_value(sql)}
|
48
48
|
end
|
49
49
|
|
50
|
-
def
|
50
|
+
def execute_select(sql, &block)
|
51
51
|
@logger.info(sql) if @logger
|
52
52
|
@pool.hold {|conn| conn.query(sql, &block)}
|
53
53
|
end
|
@@ -105,7 +105,7 @@ module Sequel
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def fetch_rows(sql, &block)
|
108
|
-
@db.
|
108
|
+
@db.execute_select(sql) do |result|
|
109
109
|
@columns = result.columns.map {|c| c.to_sym}
|
110
110
|
column_count = @columns.size
|
111
111
|
result.each do |values|
|