og 0.16.0 → 0.17.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 +485 -0
- data/README +35 -12
- data/Rakefile +4 -7
- data/benchmark/bench.rb +1 -1
- data/doc/AUTHORS +3 -3
- data/doc/RELEASES +153 -2
- data/doc/config.txt +0 -7
- data/doc/tutorial.txt +7 -0
- data/examples/README +5 -0
- data/examples/mysql_to_psql.rb +25 -50
- data/examples/run.rb +62 -77
- data/install.rb +1 -1
- data/lib/og.rb +45 -106
- data/lib/og/collection.rb +156 -0
- data/lib/og/entity.rb +131 -0
- data/lib/og/errors.rb +10 -15
- data/lib/og/manager.rb +115 -0
- data/lib/og/{mixins → mixin}/hierarchical.rb +43 -37
- data/lib/og/{mixins → mixin}/orderable.rb +35 -35
- data/lib/og/{mixins → mixin}/timestamped.rb +0 -6
- data/lib/og/{mixins → mixin}/tree.rb +0 -4
- data/lib/og/relation.rb +178 -0
- data/lib/og/relation/belongs_to.rb +14 -0
- data/lib/og/relation/has_many.rb +62 -0
- data/lib/og/relation/has_one.rb +17 -0
- data/lib/og/relation/joins_many.rb +69 -0
- data/lib/og/relation/many_to_many.rb +17 -0
- data/lib/og/relation/refers_to.rb +31 -0
- data/lib/og/store.rb +223 -0
- data/lib/og/store/filesys.rb +113 -0
- data/lib/og/store/madeleine.rb +4 -0
- data/lib/og/store/memory.rb +291 -0
- data/lib/og/store/mysql.rb +283 -0
- data/lib/og/store/psql.rb +238 -0
- data/lib/og/store/sql.rb +599 -0
- data/lib/og/store/sqlite.rb +190 -0
- data/lib/og/store/sqlserver.rb +262 -0
- data/lib/og/types.rb +19 -0
- data/lib/og/validation.rb +0 -4
- data/test/og/{mixins → mixin}/tc_hierarchical.rb +21 -23
- data/test/og/{mixins → mixin}/tc_orderable.rb +15 -14
- data/test/og/mixin/tc_timestamped.rb +38 -0
- data/test/og/store/tc_filesys.rb +71 -0
- data/test/og/tc_relation.rb +36 -0
- data/test/og/tc_store.rb +290 -0
- data/test/og/tc_types.rb +21 -0
- metadata +54 -40
- data/examples/mock_example.rb +0 -50
- data/lib/og/adapters/base.rb +0 -706
- data/lib/og/adapters/filesys.rb +0 -117
- data/lib/og/adapters/mysql.rb +0 -350
- data/lib/og/adapters/oracle.rb +0 -368
- data/lib/og/adapters/psql.rb +0 -272
- data/lib/og/adapters/sqlite.rb +0 -265
- data/lib/og/adapters/sqlserver.rb +0 -356
- data/lib/og/database.rb +0 -290
- data/lib/og/enchant.rb +0 -149
- data/lib/og/meta.rb +0 -407
- data/lib/og/testing/mock.rb +0 -165
- data/lib/og/typemacros.rb +0 -24
- data/test/og/adapters/tc_filesys.rb +0 -83
- data/test/og/adapters/tc_sqlite.rb +0 -86
- data/test/og/adapters/tc_sqlserver.rb +0 -96
- data/test/og/tc_automanage.rb +0 -46
- data/test/og/tc_lifecycle.rb +0 -105
- data/test/og/tc_many_to_many.rb +0 -61
- data/test/og/tc_meta.rb +0 -55
- data/test/og/tc_validation.rb +0 -89
- data/test/tc_og.rb +0 -364
data/lib/og/store/sql.rb
ADDED
@@ -0,0 +1,599 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Og
|
4
|
+
|
5
|
+
module SqlUtils
|
6
|
+
|
7
|
+
# Escape an SQL string
|
8
|
+
|
9
|
+
def escape(str)
|
10
|
+
return nil unless str
|
11
|
+
return str.gsub(/'/, "''")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convert a ruby time to an sql timestamp.
|
15
|
+
#--
|
16
|
+
# TODO: Optimize this
|
17
|
+
#++
|
18
|
+
|
19
|
+
def timestamp(time = Time.now)
|
20
|
+
return nil unless time
|
21
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Output YYY-mm-dd
|
25
|
+
#--
|
26
|
+
# TODO: Optimize this.
|
27
|
+
#++
|
28
|
+
|
29
|
+
def date(date)
|
30
|
+
return nil unless date
|
31
|
+
return "#{date.year}-#{date.month}-#{date.mday}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse sql datetime
|
35
|
+
#--
|
36
|
+
# TODO: Optimize this.
|
37
|
+
#++
|
38
|
+
|
39
|
+
def parse_timestamp(str)
|
40
|
+
return nil unless str
|
41
|
+
return Time.parse(str)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Input YYYY-mm-dd
|
45
|
+
#--
|
46
|
+
# TODO: Optimize this.
|
47
|
+
#++
|
48
|
+
|
49
|
+
def parse_date(str)
|
50
|
+
return nil unless str
|
51
|
+
return Date.strptime(str)
|
52
|
+
end
|
53
|
+
|
54
|
+
def quote(val)
|
55
|
+
case val
|
56
|
+
when Fixnum, Integer, Float
|
57
|
+
val ? val.to_s : 'NULL'
|
58
|
+
when String
|
59
|
+
val ? "'#{escape(val)}'" : 'NULL'
|
60
|
+
when Time
|
61
|
+
val ? "'#{timestamp(val)}'" : 'NULL'
|
62
|
+
when Date
|
63
|
+
val ? "'#{date(val)}'" : 'NULL'
|
64
|
+
when TrueClass
|
65
|
+
val ? "'t'" : 'NULL'
|
66
|
+
else
|
67
|
+
# gmosx: keep the '' for nil symbols.
|
68
|
+
val ? escape(val.to_yaml) : ''
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def table(klass)
|
73
|
+
"#{Og.table_prefix}#{klass.to_s.gsub(/::/, "_").downcase}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def join_table(class1, class2, postfix = nil)
|
77
|
+
if class1.to_s < class2.to_s
|
78
|
+
return "j#{table(class1)}#{table(class2)}#{postfix}", 1, 2
|
79
|
+
else
|
80
|
+
return "j#{table(class2)}#{table(class1)}#{postfix}", 2, 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# A Store that persists objects into a PostgreSQL database.
|
86
|
+
|
87
|
+
class SqlStore < Store
|
88
|
+
extend SqlUtils
|
89
|
+
include SqlUtils
|
90
|
+
|
91
|
+
# The connection to the backend SQL RDBMS.
|
92
|
+
|
93
|
+
attr_accessor :conn
|
94
|
+
|
95
|
+
def initialize(options)
|
96
|
+
super
|
97
|
+
|
98
|
+
# The default Ruby <-> SQL type mappings, should be valid for most
|
99
|
+
# RDBM systems.
|
100
|
+
|
101
|
+
@typemap = {
|
102
|
+
Integer => 'integer',
|
103
|
+
Fixnum => 'integer',
|
104
|
+
Float => 'float',
|
105
|
+
String => 'text',
|
106
|
+
Time => 'timestamp',
|
107
|
+
Date => 'date',
|
108
|
+
TrueClass => 'boolean',
|
109
|
+
Object => 'text',
|
110
|
+
Array => 'text',
|
111
|
+
Hash => 'text'
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
#--
|
116
|
+
# FIXME: not working.
|
117
|
+
#++
|
118
|
+
|
119
|
+
def enable_logging
|
120
|
+
require 'glue/aspects'
|
121
|
+
klass = self.class
|
122
|
+
klass.send :include, Glue::Aspects
|
123
|
+
klass.pre "Logger.info sql", :on => [:exec, :query]
|
124
|
+
Glue::Aspects.wrap(klass, [:exec, :query])
|
125
|
+
end
|
126
|
+
|
127
|
+
# Enchants a class.
|
128
|
+
|
129
|
+
def enchant(klass, manager)
|
130
|
+
klass.const_set 'OGTABLE', table(klass)
|
131
|
+
klass.module_eval 'def self.table; OGTABLE; end'
|
132
|
+
|
133
|
+
super
|
134
|
+
|
135
|
+
create_table(klass) if Og.create_schema
|
136
|
+
|
137
|
+
eval_og_insert(klass)
|
138
|
+
eval_og_update(klass)
|
139
|
+
eval_og_read(klass)
|
140
|
+
eval_og_delete(klass)
|
141
|
+
end
|
142
|
+
|
143
|
+
# :section: Lifecycle methods.
|
144
|
+
|
145
|
+
# Loads an object from the store using the primary key.
|
146
|
+
|
147
|
+
def load(pk, klass)
|
148
|
+
res = query "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
|
149
|
+
read_one(res, klass)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Reloads an object from the store.
|
153
|
+
|
154
|
+
def reload(obj, pk)
|
155
|
+
raise 'Cannot reload unmanaged object' unless obj.saved?
|
156
|
+
res = query "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
|
157
|
+
obj.og_read(res.next, 0)
|
158
|
+
ensure
|
159
|
+
res.close if res
|
160
|
+
end
|
161
|
+
|
162
|
+
# If a properties collection is provided, only updates the
|
163
|
+
# selected properties. Pass the required properties as symbols
|
164
|
+
# or strings.
|
165
|
+
|
166
|
+
def update(obj, properties = nil)
|
167
|
+
if properties
|
168
|
+
if properties.is_a?(Array)
|
169
|
+
set = []
|
170
|
+
for p in properties
|
171
|
+
set << "#{p}=#{quote(obj.send(p))}"
|
172
|
+
end
|
173
|
+
set = set.join(',')
|
174
|
+
else
|
175
|
+
set = "#{properties}=#{quote(obj.send(properties))}"
|
176
|
+
end
|
177
|
+
exec "UPDATE #{obj.class.table} SET #{set} WHERE #{obj.class.pk_symbol}=#{obj.pk}"
|
178
|
+
else
|
179
|
+
obj.og_update(self)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Update selected properties of an object or class of
|
184
|
+
# objects.
|
185
|
+
|
186
|
+
def update_properties(target, set, options = nil)
|
187
|
+
set = set.gsub(/@/, '')
|
188
|
+
|
189
|
+
if target.is_a?(Class)
|
190
|
+
sql = "UPDATE #{target.table} SET #{set} "
|
191
|
+
if options
|
192
|
+
if condition = options[:condition] || options[:where]
|
193
|
+
sql << " WHERE #{condition}"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
exec sql
|
197
|
+
else
|
198
|
+
exec "UPDATE #{target.class.table} SET #{set} WHERE #{target.class.pk_symbol}=#{target.pk}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
alias_method :pupdate, :update_properties
|
202
|
+
alias_method :update_property, :update_properties
|
203
|
+
|
204
|
+
# Find a collection of objects.
|
205
|
+
#
|
206
|
+
# === Examples
|
207
|
+
#
|
208
|
+
# User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
|
209
|
+
# Comment.find(:include => :entry)
|
210
|
+
|
211
|
+
def find(options)
|
212
|
+
klass = options[:class]
|
213
|
+
sql = resolve_options(klass, options)
|
214
|
+
read_all(query(sql), klass, options[:include])
|
215
|
+
end
|
216
|
+
|
217
|
+
# Find one object.
|
218
|
+
|
219
|
+
def find_one(options)
|
220
|
+
klass = options[:class]
|
221
|
+
options[:limit] ||= 1
|
222
|
+
sql = resolve_options(klass, options)
|
223
|
+
read_one(query(sql), klass, options[:include])
|
224
|
+
end
|
225
|
+
|
226
|
+
def count(options)
|
227
|
+
if options.is_a?(String)
|
228
|
+
sql = options
|
229
|
+
else
|
230
|
+
sql = "SELECT COUNT(*) FROM #{options[:class]::OGTABLE}"
|
231
|
+
if condition = options[:condition]
|
232
|
+
sql << " WHERE #{condition}"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
query(sql).first_value.to_i
|
237
|
+
end
|
238
|
+
|
239
|
+
# Relate two objects through an intermediate join table.
|
240
|
+
# Typically used in joins_many and many_to_many relations.
|
241
|
+
|
242
|
+
def join(obj1, obj2, table)
|
243
|
+
if obj1.class.to_s > obj2.class.to_s
|
244
|
+
obj1, obj2 = obj2, obj1
|
245
|
+
end
|
246
|
+
|
247
|
+
exec "INSERT INTO #{table} (key1, key2) VALUES (#{obj1.pk}, #{obj2.pk})"
|
248
|
+
end
|
249
|
+
|
250
|
+
# :section: Transaction methods.
|
251
|
+
|
252
|
+
# Start a new transaction.
|
253
|
+
|
254
|
+
def start
|
255
|
+
exec('START TRANSACTION') if @transaction_nesting < 1
|
256
|
+
@transaction_nesting += 1
|
257
|
+
end
|
258
|
+
|
259
|
+
# Commit a transaction.
|
260
|
+
|
261
|
+
def commit
|
262
|
+
@transaction_nesting -= 1
|
263
|
+
exec('COMMIT') if @transaction_nesting < 1
|
264
|
+
end
|
265
|
+
|
266
|
+
# Rollback a transaction.
|
267
|
+
|
268
|
+
def rollback
|
269
|
+
@transaction_nesting -= 1
|
270
|
+
exec('ROLLBACK') if @transaction_nesting < 1
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
def create_table(klass)
|
276
|
+
raise 'Not implemented'
|
277
|
+
end
|
278
|
+
|
279
|
+
def drop_table(klass)
|
280
|
+
exec "DROP TABLE #{klass.table}"
|
281
|
+
end
|
282
|
+
|
283
|
+
# Create the columns that correpsond to the klass properties.
|
284
|
+
# The generated columns array is used in create_table.
|
285
|
+
# If the property has an :sql metadata this overrides the
|
286
|
+
# default mapping. If the property has an :extra_sql metadata
|
287
|
+
# the extra sql is appended after the default mapping.
|
288
|
+
|
289
|
+
def columns_for_class(klass)
|
290
|
+
columns = []
|
291
|
+
|
292
|
+
klass.__props.each do |p|
|
293
|
+
klass.index(p.symbol) if p.meta[:index]
|
294
|
+
|
295
|
+
column = p.symbol.to_s
|
296
|
+
|
297
|
+
if p.meta and p.meta[:sql]
|
298
|
+
column << " #{p.meta[:sql]}"
|
299
|
+
else
|
300
|
+
column << " #{type_for_class(p.klass)}"
|
301
|
+
|
302
|
+
if p.meta
|
303
|
+
if default = p.meta[:default]
|
304
|
+
column << " DEFAULT #{default.inspect} NOT NULL"
|
305
|
+
end
|
306
|
+
|
307
|
+
column << " UNIQUE" if p.meta[:unique]
|
308
|
+
|
309
|
+
if extra_sql = p.meta[:extra_sql]
|
310
|
+
column << " #{extra_sql}"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
columns << column
|
316
|
+
end
|
317
|
+
|
318
|
+
return columns
|
319
|
+
end
|
320
|
+
|
321
|
+
def type_for_class(klass)
|
322
|
+
@typemap[klass]
|
323
|
+
end
|
324
|
+
|
325
|
+
# Return an sql string evaluator for the property.
|
326
|
+
# No need to optimize this, used only to precalculate code.
|
327
|
+
# YAML is used to store general Ruby objects to be more
|
328
|
+
# portable.
|
329
|
+
#--
|
330
|
+
# FIXME: add extra handling for float.
|
331
|
+
#++
|
332
|
+
|
333
|
+
def write_prop(p)
|
334
|
+
if p.klass.ancestors.include?(Integer)
|
335
|
+
return "#\{@#{p.symbol} || 'NULL'\}"
|
336
|
+
elsif p.klass.ancestors.include?(Float)
|
337
|
+
return "#\{@#{p.symbol} || 'NULL'\}"
|
338
|
+
elsif p.klass.ancestors.include?(String)
|
339
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
|
340
|
+
elsif p.klass.ancestors.include?(Time)
|
341
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
|
342
|
+
elsif p.klass.ancestors.include?(Date)
|
343
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
|
344
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
345
|
+
return "#\{@#{p.symbol} ? \"'t'\" : 'NULL' \}"
|
346
|
+
else
|
347
|
+
# gmosx: keep the '' for nil symbols.
|
348
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Return an evaluator for reading the property.
|
353
|
+
# No need to optimize this, used only to precalculate code.
|
354
|
+
|
355
|
+
def read_prop(p, col)
|
356
|
+
if p.klass.ancestors.include?(Integer)
|
357
|
+
return "res[#{col} + offset].to_i"
|
358
|
+
elsif p.klass.ancestors.include?(Float)
|
359
|
+
return "res[#{col} + offset].to_f"
|
360
|
+
elsif p.klass.ancestors.include?(String)
|
361
|
+
return "res[#{col} + offset]"
|
362
|
+
elsif p.klass.ancestors.include?(Time)
|
363
|
+
return "#{self.class}.parse_timestamp(res[#{col} + offset])"
|
364
|
+
elsif p.klass.ancestors.include?(Date)
|
365
|
+
return "#{self.class}.parse_date(res[#{col} + offset])"
|
366
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
367
|
+
return "('0' != res[#{col} + offset])"
|
368
|
+
else
|
369
|
+
return "YAML::load(res[#{col} + offset])"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# :section: Lifecycle method compilers.
|
374
|
+
|
375
|
+
# Compile the og_update method for the class.
|
376
|
+
|
377
|
+
def eval_og_insert(klass)
|
378
|
+
pk = klass.pk_symbol
|
379
|
+
props = klass.properties
|
380
|
+
values = props.collect { |p| write_prop(p) }.join(',')
|
381
|
+
|
382
|
+
sql = "INSERT INTO #{klass.table} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
|
383
|
+
|
384
|
+
klass.module_eval %{
|
385
|
+
def og_insert(store)
|
386
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
387
|
+
store.exec "#{sql}"
|
388
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
389
|
+
end
|
390
|
+
}
|
391
|
+
end
|
392
|
+
|
393
|
+
# Compile the og_update method for the class.
|
394
|
+
|
395
|
+
def eval_og_update(klass)
|
396
|
+
pk = klass.pk_symbol
|
397
|
+
props = klass.properties.reject { |p| pk == p.symbol }
|
398
|
+
|
399
|
+
updates = props.collect { |p|
|
400
|
+
"#{p.symbol}=#{write_prop(p)}"
|
401
|
+
}
|
402
|
+
|
403
|
+
sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
|
404
|
+
|
405
|
+
klass.module_eval %{
|
406
|
+
def og_update(store)
|
407
|
+
#{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
|
408
|
+
store.exec "#{sql}"
|
409
|
+
#{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
|
410
|
+
end
|
411
|
+
}
|
412
|
+
end
|
413
|
+
|
414
|
+
# Compile the og_read method for the class. This method is
|
415
|
+
# used to read (deserialize) the given class from the store.
|
416
|
+
# In order to allow for changing column/attribute orders a
|
417
|
+
# column mapping hash is used.
|
418
|
+
|
419
|
+
def eval_og_read(klass)
|
420
|
+
code = []
|
421
|
+
props = klass.properties
|
422
|
+
column_map = create_column_map(klass)
|
423
|
+
|
424
|
+
props.each do |p|
|
425
|
+
if col = column_map[p.symbol]
|
426
|
+
code << "@#{p.symbol} = #{read_prop(p, col)}"
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
code = code.join('; ')
|
431
|
+
|
432
|
+
klass.module_eval %{
|
433
|
+
def og_read(res, row = 0, offset = 0)
|
434
|
+
#{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
|
435
|
+
#{code}
|
436
|
+
#{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
|
437
|
+
end
|
438
|
+
}
|
439
|
+
end
|
440
|
+
|
441
|
+
#--
|
442
|
+
# FIXME: is pk needed as parameter?
|
443
|
+
#++
|
444
|
+
|
445
|
+
def eval_og_delete(klass)
|
446
|
+
klass.module_eval %{
|
447
|
+
def og_delete(store, pk, cascade = true)
|
448
|
+
#{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
|
449
|
+
pk ||= @#{klass.pk_symbol}
|
450
|
+
transaction do |tx|
|
451
|
+
tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
|
452
|
+
if cascade and #{klass}.__meta[:descendants]
|
453
|
+
#{klass}.__meta[:descendants].each do |dclass, foreign_key|
|
454
|
+
tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
#{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
|
459
|
+
end
|
460
|
+
}
|
461
|
+
end
|
462
|
+
|
463
|
+
# :section: Misc methods.
|
464
|
+
|
465
|
+
def handle_sql_exception(ex, sql = nil)
|
466
|
+
Logger.error "DB error #{ex}, [#{sql}]"
|
467
|
+
Logger.error ex.backtrace.join("\n")
|
468
|
+
raise StoreException.new(ex, sql) if Og.raise_store_exceptions
|
469
|
+
|
470
|
+
# FIXME: should return :error or something.
|
471
|
+
return nil
|
472
|
+
end
|
473
|
+
|
474
|
+
def resolve_options(klass, options)
|
475
|
+
tables = [klass::OGTABLE]
|
476
|
+
|
477
|
+
if included = options[:include]
|
478
|
+
join_conditions = []
|
479
|
+
|
480
|
+
for name in [included].flatten
|
481
|
+
if rel = klass.relation(name)
|
482
|
+
target_table = rel[:target_class]::OGTABLE
|
483
|
+
tables << target_table
|
484
|
+
join_conditions << "#{klass::OGTABLE}.#{rel[:foreign_key]}=#{target_table}.#{rel[:target_pk]}"
|
485
|
+
else
|
486
|
+
raise 'Unknown relation name'
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
columns = tables.collect { |t| "#{t}.*" }.join(',')
|
491
|
+
|
492
|
+
if options[:condition]
|
493
|
+
options[:condition] += " AND #{join_conditions.join(' AND ')}"
|
494
|
+
else
|
495
|
+
options[:condition] = join_conditions.join(' AND ')
|
496
|
+
end
|
497
|
+
else
|
498
|
+
columns = '*'
|
499
|
+
end
|
500
|
+
|
501
|
+
if join_table = options[:join_table]
|
502
|
+
tables << join_table
|
503
|
+
if options[:condition]
|
504
|
+
options[:condition] += " AND #{options[:join_condition]}"
|
505
|
+
else
|
506
|
+
options[:condition] = options[:join_condition]
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
sql = "SELECT #{columns} FROM #{tables.join(',')}"
|
511
|
+
|
512
|
+
if condition = options[:condition] || options[:where]
|
513
|
+
sql << " WHERE #{condition}"
|
514
|
+
end
|
515
|
+
|
516
|
+
if order = options[:order]
|
517
|
+
sql << " ORDER BY #{order}"
|
518
|
+
end
|
519
|
+
|
520
|
+
if offset = options[:offset]
|
521
|
+
sql << " OFFSET #{offset}"
|
522
|
+
end
|
523
|
+
|
524
|
+
if limit = options[:limit]
|
525
|
+
sql << " LIMIT #{limit}"
|
526
|
+
end
|
527
|
+
|
528
|
+
if extra = options[:extra]
|
529
|
+
sql << " #{extra}"
|
530
|
+
end
|
531
|
+
|
532
|
+
return sql
|
533
|
+
end
|
534
|
+
|
535
|
+
# :section: Deserialization methods.
|
536
|
+
|
537
|
+
# Deserialize the join relations.
|
538
|
+
|
539
|
+
def read_join_relations(obj, res, row, join_relations)
|
540
|
+
offset = obj.class.properties.size
|
541
|
+
|
542
|
+
for rel in join_relations
|
543
|
+
rel_obj = rel[:target_class].allocate
|
544
|
+
rel_obj.og_read(res, row, offset)
|
545
|
+
offset += rel_obj.class.properties.size
|
546
|
+
obj.instance_variable_set("@#{rel[:name]}", rel_obj)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
# Deserialize one object from the ResultSet.
|
551
|
+
|
552
|
+
def read_one(res, klass, join_relations = nil)
|
553
|
+
return nil if res.blank?
|
554
|
+
|
555
|
+
if join_relations
|
556
|
+
join_relations = [join_relations].flatten.collect do |n|
|
557
|
+
klass.relation(n)
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
row = res.next
|
562
|
+
|
563
|
+
obj = klass.allocate
|
564
|
+
obj.og_read(row)
|
565
|
+
read_join_relations(obj, row, 0, join_relations) if join_relations
|
566
|
+
|
567
|
+
res.close
|
568
|
+
|
569
|
+
return obj
|
570
|
+
end
|
571
|
+
|
572
|
+
# Deserialize all objects from the ResultSet.
|
573
|
+
|
574
|
+
def read_all(res, klass, join_relations)
|
575
|
+
return [] if res.blank?
|
576
|
+
|
577
|
+
if join_relations
|
578
|
+
join_relations = [join_relations].flatten.collect do |n|
|
579
|
+
klass.relation(n)
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
objects = []
|
584
|
+
|
585
|
+
res.each_row do |res_row, row|
|
586
|
+
obj = klass.allocate
|
587
|
+
obj.og_read(res_row, row)
|
588
|
+
read_join_relations(obj, res_row, row, join_relations) if join_relations
|
589
|
+
objects << obj
|
590
|
+
end
|
591
|
+
|
592
|
+
res.close
|
593
|
+
|
594
|
+
return objects
|
595
|
+
end
|
596
|
+
|
597
|
+
end
|
598
|
+
|
599
|
+
end
|