og 0.20.0 → 0.21.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 +796 -664
- data/INSTALL +24 -24
- data/README +39 -32
- data/Rakefile +41 -42
- data/benchmark/bench.rb +36 -36
- data/doc/AUTHORS +15 -12
- data/doc/LICENSE +3 -3
- data/doc/RELEASES +311 -243
- data/doc/config.txt +1 -1
- data/examples/mysql_to_psql.rb +15 -15
- data/examples/run.rb +92 -92
- data/install.rb +7 -17
- data/lib/og.rb +76 -75
- data/lib/og/collection.rb +203 -160
- data/lib/og/entity.rb +168 -169
- data/lib/og/errors.rb +5 -5
- data/lib/og/manager.rb +179 -178
- data/lib/og/mixin/hierarchical.rb +107 -107
- data/lib/og/mixin/optimistic_locking.rb +36 -36
- data/lib/og/mixin/orderable.rb +148 -148
- data/lib/og/mixin/timestamped.rb +8 -8
- data/lib/og/mixin/tree.rb +124 -124
- data/lib/og/relation.rb +237 -213
- data/lib/og/relation/belongs_to.rb +5 -5
- data/lib/og/relation/has_many.rb +60 -58
- data/lib/og/relation/joins_many.rb +93 -47
- data/lib/og/relation/refers_to.rb +25 -21
- data/lib/og/store.rb +210 -207
- data/lib/og/store/filesys.rb +79 -79
- data/lib/og/store/kirby.rb +263 -258
- data/lib/og/store/memory.rb +261 -261
- data/lib/og/store/mysql.rb +288 -284
- data/lib/og/store/psql.rb +261 -244
- data/lib/og/store/sql.rb +873 -720
- data/lib/og/store/sqlite.rb +177 -175
- data/lib/og/store/sqlserver.rb +204 -214
- data/lib/og/types.rb +1 -1
- data/lib/og/validation.rb +57 -57
- data/lib/vendor/mysql.rb +376 -376
- data/lib/vendor/mysql411.rb +10 -10
- data/test/og/mixin/tc_hierarchical.rb +59 -59
- data/test/og/mixin/tc_optimistic_locking.rb +40 -40
- data/test/og/mixin/tc_orderable.rb +67 -67
- data/test/og/mixin/tc_timestamped.rb +19 -19
- data/test/og/store/tc_filesys.rb +46 -46
- data/test/og/tc_inheritance.rb +81 -81
- data/test/og/tc_join.rb +67 -0
- data/test/og/tc_polymorphic.rb +49 -49
- data/test/og/tc_relation.rb +57 -30
- data/test/og/tc_select.rb +49 -0
- data/test/og/tc_store.rb +345 -337
- data/test/og/tc_types.rb +11 -11
- metadata +11 -18
data/lib/og/store/sql.rb
CHANGED
@@ -6,736 +6,888 @@ module Og
|
|
6
6
|
|
7
7
|
module SqlUtils
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
9
|
+
# Escape an SQL string
|
10
|
+
|
11
|
+
def escape(str)
|
12
|
+
return nil unless str
|
13
|
+
return str.gsub(/'/, "''")
|
14
|
+
end
|
15
|
+
|
16
|
+
# Convert a ruby time to an sql timestamp.
|
17
|
+
#--
|
18
|
+
# TODO: Optimize this
|
19
|
+
#++
|
20
|
+
|
21
|
+
def timestamp(time = Time.now)
|
22
|
+
return nil unless time
|
23
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Output YYY-mm-dd
|
27
|
+
#--
|
28
|
+
# TODO: Optimize this.
|
29
|
+
#++
|
30
|
+
|
31
|
+
def date(date)
|
32
|
+
return nil unless date
|
33
|
+
return "#{date.year}-#{date.month}-#{date.mday}"
|
34
|
+
end
|
35
|
+
|
36
|
+
#--
|
37
|
+
# TODO: implement me!
|
38
|
+
#++
|
39
|
+
|
40
|
+
def blob(val)
|
41
|
+
val
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parse an integer.
|
45
|
+
|
46
|
+
def parse_int(int)
|
47
|
+
int = int.to_i if int
|
48
|
+
int
|
49
|
+
end
|
50
|
+
|
51
|
+
# Parse a float.
|
52
|
+
|
53
|
+
def parse_float(fl)
|
54
|
+
fl = fl.to_f if fl
|
55
|
+
fl
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parse sql datetime
|
59
|
+
#--
|
60
|
+
# TODO: Optimize this.
|
61
|
+
#++
|
62
|
+
|
63
|
+
def parse_timestamp(str)
|
64
|
+
return nil unless str
|
65
|
+
return Time.parse(str)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Input YYYY-mm-dd
|
69
|
+
#--
|
70
|
+
# TODO: Optimize this.
|
71
|
+
#++
|
72
|
+
|
73
|
+
def parse_date(str)
|
74
|
+
return nil unless str
|
75
|
+
return Date.strptime(str)
|
76
|
+
end
|
77
|
+
|
78
|
+
#--
|
79
|
+
# TODO: implement me!!
|
80
|
+
#++
|
81
|
+
|
82
|
+
def parse_blob(val)
|
83
|
+
val
|
84
|
+
end
|
85
|
+
|
86
|
+
# Escape the various Ruby types.
|
87
|
+
|
88
|
+
def quote(val)
|
89
|
+
case val
|
90
|
+
when Fixnum, Integer, Float
|
91
|
+
val ? val.to_s : 'NULL'
|
92
|
+
when String
|
93
|
+
val ? "'#{escape(val)}'" : 'NULL'
|
94
|
+
when Time
|
95
|
+
val ? "'#{timestamp(val)}'" : 'NULL'
|
96
|
+
when Date
|
97
|
+
val ? "'#{date(val)}'" : 'NULL'
|
98
|
+
when TrueClass
|
99
|
+
val ? "'t'" : 'NULL'
|
100
|
+
else
|
101
|
+
# gmosx: keep the '' for nil symbols.
|
102
|
+
val ? escape(val.to_yaml) : ''
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Apply table name conventions to a class name.
|
107
|
+
|
108
|
+
def tableize(klass)
|
109
|
+
"#{klass.to_s.gsub(/::/, "_").downcase}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def table(klass)
|
113
|
+
"#{Og.table_prefix}#{tableize(klass)}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def join_object_ordering(obj1, obj2)
|
117
|
+
if obj1.class.to_s <= obj2.class.to_s
|
118
|
+
return obj1, obj2
|
119
|
+
else
|
120
|
+
return obj2, obj1, true
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def join_class_ordering(class1, class2)
|
125
|
+
if class1.to_s <= class2.to_s
|
126
|
+
return class1, class2
|
127
|
+
else
|
128
|
+
return class2, class1, true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_join_name(class1, class2, postfix = nil)
|
133
|
+
# Don't reorder arguments, as this is used in places that
|
134
|
+
# have already determined the order they want.
|
135
|
+
"#{Og.table_prefix}j_#{tableize(class1)}_#{tableize(class2)}#{postfix}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def join_table(class1, class2, postfix = nil)
|
139
|
+
first, second = join_class_ordering(class1, class2)
|
140
|
+
build_join_name(first, second, postfix)
|
141
|
+
end
|
142
|
+
|
143
|
+
def join_table_index(key)
|
144
|
+
"#{key}_idx"
|
145
|
+
end
|
146
|
+
|
147
|
+
def join_table_key(klass)
|
148
|
+
"#{klass.to_s.split('::').last.downcase}_oid"
|
149
|
+
end
|
150
|
+
def join_table_keys(class1, class2)
|
151
|
+
if class1 == class2
|
152
|
+
# Fix for the self-join case.
|
153
|
+
return join_table_key(class1), "#{join_table_key(class2)}2"
|
154
|
+
else
|
155
|
+
return join_table_key(class1), join_table_key(class2)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def join_table_info(owner_class, target_class, postfix = nil)
|
160
|
+
owner_key, target_key = join_table_keys(owner_class, target_class)
|
161
|
+
first, second, changed = join_class_ordering(owner_class, target_class)
|
162
|
+
|
163
|
+
if changed
|
164
|
+
first_key, second_key = target_key, owner_key
|
165
|
+
else
|
166
|
+
first_key, second_key = owner_key, target_key
|
167
|
+
end
|
168
|
+
|
169
|
+
return {
|
170
|
+
:table => join_table(owner_class, target_class, postfix),
|
171
|
+
:owner_key => owner_key,
|
172
|
+
:target_key => target_key,
|
173
|
+
:first_table => table(first),
|
174
|
+
:first_key => first_key,
|
175
|
+
:first_index => join_table_index(first_key),
|
176
|
+
:second_table => table(second),
|
177
|
+
:second_key => second_key,
|
178
|
+
:second_index => join_table_index(second_key)
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
# Subclasses can override this if they need a different
|
183
|
+
# syntax.
|
184
|
+
|
185
|
+
def create_join_table_sql(join_table_info, suffix = 'NOT NULL', key_type = 'integer')
|
186
|
+
join_table = join_table_info[:table]
|
187
|
+
first_index = join_table_info[:first_index]
|
188
|
+
first_key = join_table_info[:first_key]
|
189
|
+
second_key = join_table_info[:second_key]
|
190
|
+
second_index = join_table_info[:second_index]
|
191
|
+
|
192
|
+
sql = []
|
193
|
+
|
194
|
+
sql << %{
|
195
|
+
CREATE TABLE #{join_table} (
|
196
|
+
#{first_key} integer NOT NULL,
|
197
|
+
#{second_key} integer NOT NULL,
|
198
|
+
PRIMARY KEY(#{first_key}, #{second_key})
|
199
|
+
)
|
200
|
+
}
|
201
|
+
|
202
|
+
# gmosx: not that useful?
|
203
|
+
# sql << "CREATE INDEX #{first_index} ON #{join_table} (#{first_key})"
|
204
|
+
# sql << "CREATE INDEX #{second_index} ON #{join_table} (#{second_key})"
|
205
|
+
|
206
|
+
return sql
|
207
|
+
end
|
208
|
+
|
117
209
|
end
|
118
210
|
|
119
|
-
#
|
211
|
+
# An abstract SQL powered store.
|
120
212
|
|
121
213
|
class SqlStore < Store
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
214
|
+
extend SqlUtils
|
215
|
+
include SqlUtils
|
216
|
+
|
217
|
+
# The connection to the backend SQL RDBMS.
|
218
|
+
|
219
|
+
attr_accessor :conn
|
220
|
+
|
221
|
+
def initialize(options)
|
222
|
+
super
|
223
|
+
|
224
|
+
# The default Ruby <-> SQL type mappings, should be valid for most
|
225
|
+
# RDBM systems.
|
226
|
+
|
227
|
+
@typemap = {
|
228
|
+
Integer => 'integer',
|
229
|
+
Fixnum => 'integer',
|
230
|
+
Float => 'float',
|
231
|
+
String => 'text',
|
232
|
+
Time => 'timestamp',
|
233
|
+
Date => 'date',
|
234
|
+
TrueClass => 'boolean',
|
235
|
+
Object => 'text',
|
236
|
+
Array => 'text',
|
237
|
+
Hash => 'text',
|
238
|
+
Og::Blob => 'bytea' # psql
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
#--
|
243
|
+
# FIXME: not working.
|
244
|
+
#++
|
245
|
+
|
246
|
+
def enable_logging
|
247
|
+
require 'glue/aspects'
|
248
|
+
klass = self.class
|
249
|
+
klass.send :include, Glue::Aspects
|
250
|
+
klass.pre "Logger.info sql", :on => [:exec, :query]
|
251
|
+
Glue::Aspects.wrap(klass, [:exec, :query])
|
252
|
+
end
|
253
|
+
|
254
|
+
# Enchants a class.
|
255
|
+
|
256
|
+
def enchant(klass, manager)
|
257
|
+
|
258
|
+
# setup the table where this class is mapped.
|
259
|
+
|
260
|
+
if sclass = klass.metadata.superclass
|
261
|
+
klass.const_set 'OGTABLE', table(sclass.first)
|
262
|
+
else
|
263
|
+
klass.const_set 'OGTABLE', table(klass)
|
264
|
+
end
|
265
|
+
|
266
|
+
klass.module_eval 'def self.table; OGTABLE; end'
|
267
|
+
|
268
|
+
# precompile a class specific allocate method. If this
|
269
|
+
# is an STI parent classes it reads the class from the
|
270
|
+
# resultset.
|
271
|
+
|
272
|
+
if klass.metadata.subclasses
|
273
|
+
klass.module_eval %{
|
274
|
+
def self.og_allocate(res)
|
275
|
+
Object.constant(res[0]).allocate
|
276
|
+
end
|
277
|
+
}
|
278
|
+
else
|
279
|
+
klass.module_eval %{
|
280
|
+
def self.og_allocate(res)
|
281
|
+
self.allocate
|
282
|
+
end
|
283
|
+
}
|
284
|
+
end
|
285
|
+
|
286
|
+
super
|
287
|
+
|
288
|
+
unless klass.polymorphic_parent?
|
289
|
+
# create the table if needed.
|
290
|
+
|
291
|
+
eval_og_create_schema(klass)
|
292
|
+
# create_table(klass) if Og.create_schema
|
293
|
+
klass.allocate.og_create_schema(self)
|
294
|
+
|
295
|
+
# precompile class specific lifecycle methods.
|
296
|
+
|
297
|
+
eval_og_insert(klass)
|
298
|
+
eval_og_update(klass)
|
299
|
+
eval_og_read(klass)
|
300
|
+
eval_og_delete(klass)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# :section: Lifecycle methods.
|
305
|
+
|
306
|
+
# Loads an object from the store using the primary key.
|
307
|
+
|
308
|
+
def load(pk, klass)
|
309
|
+
res = query "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
|
310
|
+
read_one(res, klass)
|
311
|
+
end
|
312
|
+
alias_method :exist?, :load
|
313
|
+
|
314
|
+
# Reloads an object from the store.
|
315
|
+
|
316
|
+
def reload(obj, pk)
|
317
|
+
raise 'Cannot reload unmanaged object' unless obj.saved?
|
318
|
+
res = query "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
|
319
|
+
obj.og_read(res.next, 0)
|
320
|
+
ensure
|
321
|
+
res.close if res
|
322
|
+
end
|
323
|
+
|
324
|
+
# If a properties collection is provided, only updates the
|
325
|
+
# selected properties. Pass the required properties as symbols
|
326
|
+
# or strings.
|
327
|
+
|
328
|
+
def update(obj, options = nil)
|
329
|
+
if options and properties = options[:only]
|
330
|
+
if properties.is_a?(Array)
|
331
|
+
set = []
|
332
|
+
for p in properties
|
333
|
+
set << "#{p}=#{quote(obj.send(p))}"
|
334
|
+
end
|
335
|
+
set = set.join(',')
|
336
|
+
else
|
337
|
+
set = "#{properties}=#{quote(obj.send(properties))}"
|
338
|
+
end
|
339
|
+
sql = "UPDATE #{obj.class.table} SET #{set} WHERE #{obj.class.pk_symbol}=#{obj.pk}"
|
340
|
+
sql << " AND #{options[:condition]}" if options[:condition]
|
341
|
+
sql_update(sql)
|
342
|
+
else
|
343
|
+
obj.og_update(self, options)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Update selected properties of an object or class of
|
348
|
+
# objects.
|
349
|
+
|
350
|
+
def update_properties(target, set, options = nil)
|
351
|
+
set = set.gsub(/@/, '')
|
352
|
+
|
353
|
+
if target.is_a?(Class)
|
354
|
+
sql = "UPDATE #{target.table} SET #{set} "
|
355
|
+
sql << " WHERE #{options[:condition]}" if options and options[:condition]
|
356
|
+
sql_update(sql)
|
357
|
+
else
|
358
|
+
sql = "UPDATE #{target.class.table} SET #{set} WHERE #{target.class.pk_symbol}=#{target.pk}"
|
359
|
+
sql << " AND #{options[:condition]}" if options and options[:condition]
|
360
|
+
sql_update(sql)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
alias_method :pupdate, :update_properties
|
364
|
+
alias_method :update_property, :update_properties
|
365
|
+
|
366
|
+
# Find a collection of objects.
|
367
|
+
#
|
368
|
+
# === Examples
|
369
|
+
#
|
370
|
+
# User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
|
371
|
+
# Comment.find(:include => :entry)
|
372
|
+
|
373
|
+
def find(options)
|
374
|
+
klass = options[:class]
|
375
|
+
sql = resolve_options(klass, options)
|
376
|
+
read_all(query(sql), klass, options)
|
377
|
+
end
|
378
|
+
|
379
|
+
# Find one object.
|
380
|
+
|
381
|
+
def find_one(options)
|
382
|
+
klass = options[:class]
|
288
383
|
# gmosx, THINK: should not set this by default.
|
289
|
-
#
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
384
|
+
# options[:limit] ||= 1
|
385
|
+
sql = resolve_options(klass, options)
|
386
|
+
read_one(query(sql), klass, options)
|
387
|
+
end
|
388
|
+
|
389
|
+
# Perform a custom sql query and deserialize the
|
390
|
+
# results.
|
391
|
+
|
392
|
+
def select(sql, klass)
|
393
|
+
sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
|
394
|
+
read_all(query(sql), klass)
|
395
|
+
end
|
396
|
+
alias_method :find_by_sql, :select
|
397
|
+
|
398
|
+
# Specialized one result version of select.
|
399
|
+
|
400
|
+
def select_one(sql, klass)
|
401
|
+
sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
|
402
|
+
read_one(query(sql), klass)
|
403
|
+
end
|
404
|
+
alias_method :find_by_sql_one, :select_one
|
405
|
+
|
406
|
+
# Perform an aggregation over query results.
|
407
|
+
|
408
|
+
def aggregate(options)
|
409
|
+
if options.is_a?(String)
|
410
|
+
sql = options
|
411
|
+
else
|
412
|
+
aggregate = options[:aggregate] || 'COUNT(*)'
|
413
|
+
sql = "SELECT #{aggregate} FROM #{options[:class].table}"
|
414
|
+
if condition = options[:condition]
|
415
|
+
sql << " WHERE #{condition}"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
query(sql).first_value.to_i
|
420
|
+
end
|
421
|
+
alias_method :count, :aggregate
|
422
|
+
|
423
|
+
# Relate two objects through an intermediate join table.
|
424
|
+
# Typically used in joins_many and many_to_many relations.
|
425
|
+
|
426
|
+
def join(obj1, obj2, table, options = nil)
|
427
|
+
first, second = join_object_ordering(obj1, obj2)
|
428
|
+
first_key, second_key = join_table_keys(obj1.class, obj2.class)
|
429
|
+
if options
|
430
|
+
exec "INSERT INTO #{table} (#{first_key},#{second_key}, #{options.keys.join(',')}) VALUES (#{first.pk},#{second.pk}, #{options.values.map { |v| quote(v) }.join(',')})"
|
431
|
+
else
|
432
|
+
exec "INSERT INTO #{table} (#{first_key},#{second_key}) VALUES (#{first.pk}, #{second.pk})"
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# Unrelate two objects be removing their relation from the
|
437
|
+
# join table.
|
438
|
+
|
439
|
+
def unjoin(obj1, obj2, table)
|
440
|
+
first, second = join_object_ordering(obj1, obj2)
|
441
|
+
first_key, second_key = join_table_keys(obj1, obj2, table)
|
442
|
+
exec "DELETE FROM #{table} WHERE #{first_key}=#{first.pk} AND #{second_key}=#{second.pk}"
|
443
|
+
end
|
444
|
+
|
445
|
+
# :section: Transaction methods.
|
446
|
+
|
447
|
+
# Start a new transaction.
|
448
|
+
|
449
|
+
def start
|
450
|
+
exec('START TRANSACTION') if @transaction_nesting < 1
|
451
|
+
@transaction_nesting += 1
|
452
|
+
end
|
453
|
+
|
454
|
+
# Commit a transaction.
|
455
|
+
|
456
|
+
def commit
|
457
|
+
@transaction_nesting -= 1
|
458
|
+
exec('COMMIT') if @transaction_nesting < 1
|
459
|
+
end
|
460
|
+
|
461
|
+
# Rollback a transaction.
|
462
|
+
|
463
|
+
def rollback
|
464
|
+
@transaction_nesting -= 1
|
465
|
+
exec('ROLLBACK') if @transaction_nesting < 1
|
466
|
+
end
|
467
|
+
|
468
|
+
# :section: Low level methods.
|
469
|
+
|
470
|
+
# Encapsulates a low level update method.
|
471
|
+
|
472
|
+
def sql_update(sql)
|
473
|
+
exec(sql)
|
474
|
+
# return affected rows.
|
475
|
+
end
|
376
476
|
|
377
477
|
private
|
378
478
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
479
|
+
def create_table(klass)
|
480
|
+
raise 'Not implemented'
|
481
|
+
end
|
482
|
+
|
483
|
+
def drop_table(klass)
|
484
|
+
exec "DROP TABLE #{klass.table}"
|
485
|
+
end
|
486
|
+
|
487
|
+
# Create the fields that correpsond to the klass properties.
|
488
|
+
# The generated fields array is used in create_table.
|
489
|
+
# If the property has an :sql metadata this overrides the
|
490
|
+
# default mapping. If the property has an :extra_sql metadata
|
491
|
+
# the extra sql is appended after the default mapping.
|
492
|
+
|
493
|
+
def fields_for_class(klass)
|
494
|
+
fields = []
|
495
|
+
properties = klass.properties
|
496
|
+
|
497
|
+
if subclasses = klass.metadata.subclasses
|
498
|
+
# This class as a superclass in a single table inheritance
|
499
|
+
# chain. So inject a special class ogtype field that
|
500
|
+
# holds the class name.
|
501
|
+
fields << "ogtype VARCHAR(30)"
|
502
|
+
|
503
|
+
for subclass in subclasses
|
504
|
+
properties.concat(subclass.properties)
|
505
|
+
end
|
506
|
+
|
507
|
+
properties.uniq!
|
508
|
+
end
|
509
|
+
|
510
|
+
properties.each do |p|
|
511
|
+
klass.index(p.symbol) if p.meta[:index]
|
512
|
+
|
513
|
+
field = p.symbol.to_s
|
514
|
+
|
515
|
+
if p.meta and p.meta[:sql]
|
516
|
+
field << " #{p.meta[:sql]}"
|
517
|
+
else
|
518
|
+
field << " #{type_for_class(p.klass)}"
|
519
|
+
|
520
|
+
if p.meta
|
521
|
+
field << " UNIQUE" if p.meta[:unique]
|
522
|
+
|
523
|
+
if default = p.meta[:default]
|
524
|
+
field << " DEFAULT #{default.inspect} NOT NULL"
|
525
|
+
end
|
526
|
+
|
527
|
+
if extra_sql = p.meta[:extra_sql]
|
528
|
+
field << " #{extra_sql}"
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
fields << field
|
534
|
+
end
|
535
|
+
|
536
|
+
return fields
|
537
|
+
end
|
538
|
+
|
539
|
+
def type_for_class(klass)
|
540
|
+
@typemap[klass]
|
541
|
+
end
|
542
|
+
|
543
|
+
# Return an sql string evaluator for the property.
|
544
|
+
# No need to optimize this, used only to precalculate code.
|
545
|
+
# YAML is used to store general Ruby objects to be more
|
546
|
+
# portable.
|
547
|
+
#--
|
548
|
+
# FIXME: add extra handling for float.
|
549
|
+
#++
|
550
|
+
|
551
|
+
def write_prop(p)
|
552
|
+
if p.klass.ancestors.include?(Integer)
|
553
|
+
return "#\{@#{p.symbol} || 'NULL'\}"
|
554
|
+
elsif p.klass.ancestors.include?(Float)
|
555
|
+
return "#\{@#{p.symbol} || 'NULL'\}"
|
556
|
+
elsif p.klass.ancestors.include?(String)
|
557
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
|
558
|
+
elsif p.klass.ancestors.include?(Time)
|
559
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
|
560
|
+
elsif p.klass.ancestors.include?(Date)
|
561
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
|
562
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
563
|
+
return "#\{@#{p.symbol} ? \"'t'\" : 'NULL' \}"
|
464
564
|
elsif p.klass.ancestors.include?(Og::Blob)
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
565
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(#{self.class}.blob(@#{p.symbol}))\}'" : 'NULL'\}|
|
566
|
+
else
|
567
|
+
# gmosx: keep the '' for nil symbols.
|
568
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
# Return an evaluator for reading the property.
|
573
|
+
# No need to optimize this, used only to precalculate code.
|
574
|
+
|
575
|
+
def read_prop(p, col)
|
576
|
+
if p.klass.ancestors.include?(Integer)
|
577
|
+
return "#{self.class}.parse_int(res[#{col} + offset])"
|
578
|
+
elsif p.klass.ancestors.include?(Float)
|
579
|
+
return "#{self.class}.parse_float(res[#{col} + offset])"
|
580
|
+
elsif p.klass.ancestors.include?(String)
|
581
|
+
return "res[#{col} + offset]"
|
582
|
+
elsif p.klass.ancestors.include?(Time)
|
583
|
+
return "#{self.class}.parse_timestamp(res[#{col} + offset])"
|
584
|
+
elsif p.klass.ancestors.include?(Date)
|
585
|
+
return "#{self.class}.parse_date(res[#{col} + offset])"
|
586
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
587
|
+
return "('0' != res[#{col} + offset])"
|
488
588
|
elsif p.klass.ancestors.include?(Og::Blob)
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
589
|
+
return "#{self.class}.parse_blob(res[#{col} + offset])"
|
590
|
+
else
|
591
|
+
return "YAML::load(res[#{col} + offset])"
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
# :section: Lifecycle method compilers.
|
596
|
+
|
597
|
+
# Compile the og_insert method for the class.
|
598
|
+
|
599
|
+
def eval_og_insert(klass)
|
600
|
+
pk = klass.pk_symbol
|
601
|
+
props = klass.properties
|
602
|
+
values = props.collect { |p| write_prop(p) }.join(',')
|
603
|
+
|
604
|
+
if klass.metadata.superclass or klass.metadata.subclasses
|
605
|
+
props << Property.new(:ogtype, String)
|
606
|
+
values << ", '#{klass}'"
|
607
|
+
end
|
608
|
+
|
609
|
+
sql = "INSERT INTO #{klass.table} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
|
610
|
+
|
611
|
+
klass.module_eval %{
|
612
|
+
def og_insert(store)
|
613
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
614
|
+
store.exec "#{sql}"
|
615
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
616
|
+
end
|
617
|
+
}
|
618
|
+
end
|
619
|
+
|
620
|
+
# Compile the og_update method for the class.
|
621
|
+
|
622
|
+
def eval_og_update(klass)
|
623
|
+
pk = klass.pk_symbol
|
624
|
+
props = klass.properties.reject { |p| pk == p.symbol }
|
625
|
+
|
626
|
+
updates = props.collect { |p|
|
627
|
+
"#{p.symbol}=#{write_prop(p)}"
|
628
|
+
}
|
629
|
+
|
630
|
+
sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
|
631
|
+
|
632
|
+
klass.module_eval %{
|
633
|
+
def og_update(store, options = nil)
|
634
|
+
#{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
|
635
|
+
sql = "#{sql}"
|
636
|
+
sql << " AND \#{options[:condition]}" if options and options[:condition]
|
637
|
+
changed = store.sql_update(sql)
|
638
|
+
#{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
|
639
|
+
return changed
|
640
|
+
end
|
641
|
+
}
|
642
|
+
end
|
643
|
+
|
644
|
+
# Compile the og_read method for the class. This method is
|
645
|
+
# used to read (deserialize) the given class from the store.
|
646
|
+
# In order to allow for changing field/attribute orders a
|
647
|
+
# field mapping hash is used.
|
648
|
+
|
649
|
+
def eval_og_read(klass)
|
650
|
+
code = []
|
651
|
+
props = klass.properties
|
652
|
+
field_map = create_field_map(klass)
|
653
|
+
|
654
|
+
props.each do |p|
|
655
|
+
if col = field_map[p.symbol]
|
656
|
+
code << "@#{p.symbol} = #{read_prop(p, col)}"
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
code = code.join('; ')
|
661
|
+
|
662
|
+
klass.module_eval %{
|
663
|
+
def og_read(res, row = 0, offset = 0)
|
664
|
+
#{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
|
665
|
+
#{code}
|
666
|
+
#{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
|
667
|
+
end
|
668
|
+
}
|
669
|
+
end
|
670
|
+
|
671
|
+
#--
|
672
|
+
# FIXME: is pk needed as parameter?
|
673
|
+
#++
|
674
|
+
|
675
|
+
def eval_og_delete(klass)
|
676
|
+
klass.module_eval %{
|
677
|
+
def og_delete(store, pk, cascade = true)
|
678
|
+
#{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
|
679
|
+
pk ||= @#{klass.pk_symbol}
|
680
|
+
transaction do |tx|
|
681
|
+
tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
|
682
|
+
if cascade and #{klass}.metadata[:descendants]
|
683
|
+
#{klass}.metadata[:descendants].each do |dclass, foreign_key|
|
684
|
+
tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
#{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
|
689
|
+
end
|
690
|
+
}
|
691
|
+
end
|
692
|
+
|
693
|
+
# Creates the schema for this class. Can be intercepted with
|
694
|
+
# aspects to add special behaviours.
|
695
|
+
|
696
|
+
def eval_og_create_schema(klass)
|
697
|
+
klass.module_eval %{
|
698
|
+
def og_create_schema(store)
|
699
|
+
#{Aspects.gen_advice_code(:og_create_schema, klass.advices, :pre) if klass.respond_to?(:advices)}
|
700
|
+
store.send(:create_table, #{klass}) if Og.create_schema
|
701
|
+
#{Aspects.gen_advice_code(:og_create_schema, klass.advices, :post) if klass.respond_to?(:advices)}
|
702
|
+
end
|
703
|
+
}
|
704
|
+
end
|
705
|
+
|
706
|
+
# :section: Misc methods.
|
707
|
+
|
708
|
+
def handle_sql_exception(ex, sql = nil)
|
709
|
+
Logger.error "DB error #{ex}, [#{sql}]"
|
710
|
+
Logger.error ex.backtrace.join("\n")
|
711
|
+
raise StoreException.new(ex, sql) if Og.raise_store_exceptions
|
712
|
+
|
713
|
+
# FIXME: should return :error or something.
|
714
|
+
return nil
|
715
|
+
end
|
716
|
+
|
717
|
+
def resolve_options(klass, options)
|
718
|
+
if sql = options[:sql]
|
719
|
+
sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
|
720
|
+
return sql
|
721
|
+
end
|
722
|
+
|
723
|
+
tables = [klass::OGTABLE]
|
724
|
+
|
725
|
+
if included = options[:include]
|
726
|
+
join_conditions = []
|
727
|
+
|
728
|
+
for name in [included].flatten
|
729
|
+
if rel = klass.relation(name)
|
730
|
+
target_table = rel[:target_class]::OGTABLE
|
731
|
+
tables << target_table
|
732
|
+
join_conditions << "#{klass::OGTABLE}.#{rel[:foreign_key]}=#{target_table}.#{rel[:target_pk]}"
|
733
|
+
else
|
734
|
+
raise 'Unknown relation name'
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
fields = tables.collect { |t| "#{t}.*" }.join(',')
|
739
|
+
|
740
|
+
update_condition options, join_conditions.join(' AND ')
|
741
|
+
elsif fields = options[:select]
|
742
|
+
# query the provided fields.
|
743
|
+
else
|
744
|
+
fields = '*'
|
745
|
+
end
|
746
|
+
|
747
|
+
if join_table = options[:join_table]
|
748
|
+
tables << join_table
|
749
|
+
update_condition options, options[:join_condition]
|
750
|
+
end
|
751
|
+
|
752
|
+
if ogtype = options[:type]
|
753
|
+
update_condition options, "ogtype='#{ogtype}'"
|
754
|
+
end
|
755
|
+
|
756
|
+
sql = "SELECT #{fields} FROM #{tables.join(',')}"
|
757
|
+
|
758
|
+
if condition = options[:condition] || options[:where]
|
759
|
+
sql << " WHERE #{condition}"
|
760
|
+
end
|
761
|
+
|
762
|
+
if order = options[:order]
|
763
|
+
sql << " ORDER BY #{order}"
|
764
|
+
end
|
765
|
+
|
766
|
+
resolve_limit_options(options, sql)
|
767
|
+
|
768
|
+
if extra = options[:extra]
|
769
|
+
sql << " #{extra}"
|
770
|
+
end
|
771
|
+
|
772
|
+
return sql
|
773
|
+
end
|
774
|
+
|
775
|
+
# Subclasses can override this if they need some other order.
|
776
|
+
# This is needed because different backends require different
|
777
|
+
# order of the keywords.
|
778
|
+
|
779
|
+
def resolve_limit_options(options, sql)
|
780
|
+
if limit = options[:limit]
|
781
|
+
sql << " LIMIT #{limit}"
|
782
|
+
|
783
|
+
if offset = options[:offset]
|
784
|
+
sql << " OFFSET #{offset}"
|
785
|
+
end
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
# :section: Deserialization methods.
|
790
|
+
|
791
|
+
# Read a field (column) from a result set row.
|
792
|
+
|
793
|
+
def read_field
|
794
|
+
end
|
795
|
+
|
796
|
+
# Dynamicaly deserializes a result set row into an object.
|
797
|
+
# Used for specialized queries or join queries. Please
|
798
|
+
# not that this deserialization method is slower than the
|
799
|
+
# precompiled og_read method.
|
800
|
+
|
801
|
+
def read_row(obj, res, res_row, row)
|
802
|
+
res.fields.each_with_index do |field, idx|
|
803
|
+
obj.instance_variable_set "@#{field}", res_row[idx]
|
804
|
+
end
|
805
|
+
end
|
806
|
+
|
807
|
+
# Deserialize the join relations.
|
808
|
+
|
809
|
+
def read_join_relations(obj, res_row, row, join_relations)
|
810
|
+
offset = obj.class.properties.size
|
811
|
+
|
812
|
+
for rel in join_relations
|
813
|
+
rel_obj = rel[:target_class].og_allocate(res_row)
|
814
|
+
rel_obj.og_read(res_row, row, offset)
|
815
|
+
offset += rel_obj.class.properties.size
|
816
|
+
obj.instance_variable_set("@#{rel[:name]}", rel_obj)
|
817
|
+
end
|
818
|
+
end
|
819
|
+
|
820
|
+
# Deserialize one object from the ResultSet.
|
821
|
+
|
822
|
+
def read_one(res, klass, options = nil)
|
823
|
+
return nil if res.blank?
|
824
|
+
|
825
|
+
if options and join_relations = options[:include]
|
826
|
+
join_relations = [join_relations].flatten.collect do |n|
|
827
|
+
klass.relation(n)
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
res_row = res.next
|
832
|
+
|
833
|
+
obj = klass.og_allocate(res_row)
|
834
|
+
|
835
|
+
if options and options[:select]
|
836
|
+
read_row(obj, res, res_row, 0)
|
837
|
+
else
|
838
|
+
obj.og_read(res_row)
|
839
|
+
read_join_relations(obj, res_row, 0, join_relations) if join_relations
|
840
|
+
end
|
841
|
+
|
842
|
+
return obj
|
843
|
+
|
844
|
+
ensure
|
845
|
+
res.close
|
846
|
+
end
|
847
|
+
|
848
|
+
# Deserialize all objects from the ResultSet.
|
849
|
+
|
850
|
+
def read_all(res, klass, options = nil)
|
851
|
+
return [] if res.blank?
|
852
|
+
|
853
|
+
if options and join_relations = options[:include]
|
854
|
+
join_relations = [join_relations].flatten.collect do |n|
|
855
|
+
klass.relation(n)
|
856
|
+
end
|
857
|
+
end
|
858
|
+
|
859
|
+
objects = []
|
860
|
+
|
861
|
+
if options and options[:select]
|
862
|
+
res.each_row do |res_row, row|
|
863
|
+
obj = klass.og_allocate(res_row)
|
864
|
+
read_row(obj, res, res_row, row)
|
865
|
+
objects << obj
|
866
|
+
end
|
867
|
+
else
|
868
|
+
res.each_row do |res_row, row|
|
869
|
+
obj = klass.og_allocate(res_row)
|
870
|
+
obj.og_read(res_row, row)
|
871
|
+
read_join_relations(obj, res_row, row, join_relations) if join_relations
|
872
|
+
objects << obj
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
return objects
|
877
|
+
|
878
|
+
ensure
|
879
|
+
res.close
|
880
|
+
end
|
881
|
+
|
882
|
+
# Helper method that updates the condition string.
|
883
|
+
|
884
|
+
def update_condition(options, cond, joiner = 'AND')
|
885
|
+
if options[:condition]
|
886
|
+
options[:condition] += " #{joiner} #{cond}"
|
887
|
+
else
|
888
|
+
options[:condition] = cond
|
889
|
+
end
|
890
|
+
end
|
739
891
|
|
740
892
|
end
|
741
893
|
|
@@ -744,3 +896,4 @@ end
|
|
744
896
|
# * George Moschovitis <gm@navel.gr>
|
745
897
|
# * Michael Neumann <mneumann@ntecs.de>
|
746
898
|
# * Ghislain Mary
|
899
|
+
# * Ysabel <deb@ysabel.org>
|