og 0.24.0 → 0.25.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/ProjectInfo +2 -5
- data/README +2 -0
- data/doc/AUTHORS +4 -1
- data/doc/RELEASES +53 -0
- data/examples/run.rb +2 -2
- data/lib/{og/mixin → glue}/hierarchical.rb +19 -19
- data/lib/{og/mixin → glue}/optimistic_locking.rb +1 -1
- data/lib/glue/orderable.rb +235 -0
- data/lib/glue/revisable.rb +2 -0
- data/lib/glue/taggable.rb +176 -0
- data/lib/{og/mixin/taggable.rb → glue/taggable_old.rb} +6 -0
- data/lib/glue/timestamped.rb +37 -0
- data/lib/{og/mixin → glue}/tree.rb +3 -8
- data/lib/og.rb +21 -20
- data/lib/og/collection.rb +15 -1
- data/lib/og/entity.rb +256 -114
- data/lib/og/manager.rb +60 -27
- data/lib/og/{mixin/schema_inheritance_base.rb → markers.rb} +5 -2
- data/lib/og/relation.rb +70 -74
- data/lib/og/relation/belongs_to.rb +5 -3
- data/lib/og/relation/has_many.rb +1 -0
- data/lib/og/relation/joins_many.rb +5 -4
- data/lib/og/store.rb +25 -46
- data/lib/og/store/alpha/filesys.rb +1 -1
- data/lib/og/store/alpha/kirby.rb +30 -30
- data/lib/og/store/alpha/memory.rb +49 -49
- data/lib/og/store/alpha/sqlserver.rb +7 -7
- data/lib/og/store/kirby.rb +38 -38
- data/lib/og/store/mysql.rb +43 -43
- data/lib/og/store/psql.rb +222 -53
- data/lib/og/store/sql.rb +165 -105
- data/lib/og/store/sqlite.rb +29 -25
- data/lib/og/validation.rb +24 -14
- data/lib/{vendor → og/vendor}/README +0 -0
- data/lib/{vendor → og/vendor}/kbserver.rb +1 -1
- data/lib/{vendor → og/vendor}/kirbybase.rb +230 -79
- data/lib/{vendor → og/vendor}/mysql.rb +0 -0
- data/lib/{vendor → og/vendor}/mysql411.rb +0 -0
- data/test/og/mixin/tc_hierarchical.rb +1 -1
- data/test/og/mixin/tc_optimistic_locking.rb +1 -1
- data/test/og/mixin/tc_orderable.rb +1 -1
- data/test/og/mixin/tc_taggable.rb +2 -2
- data/test/og/mixin/tc_timestamped.rb +2 -2
- data/test/og/tc_finder.rb +33 -0
- data/test/og/tc_inheritance.rb +2 -2
- data/test/og/tc_scoped.rb +45 -0
- data/test/og/tc_store.rb +1 -7
- metadata +21 -18
- data/lib/og/mixin/orderable.rb +0 -174
- data/lib/og/mixin/revisable.rb +0 -0
- data/lib/og/mixin/timestamped.rb +0 -24
data/lib/og/store/psql.rb
CHANGED
@@ -5,6 +5,63 @@ rescue Object => ex
|
|
5
5
|
Logger.error ex
|
6
6
|
end
|
7
7
|
|
8
|
+
class PGconn
|
9
|
+
# Lists all the tables within the database.
|
10
|
+
|
11
|
+
def list_tables
|
12
|
+
r = self.exec "SELECT c.relname FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid)"
|
13
|
+
ret = r.result.flatten
|
14
|
+
r.clear
|
15
|
+
ret
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns true if a table exists within the database, false
|
19
|
+
# otherwise.
|
20
|
+
|
21
|
+
def table_exists?(table) #rp: this should be abstracted to the sql abstractor
|
22
|
+
r = self.exec "SELECT c.relname FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) AND c.relname='#{self.class.escape(table.to_s)}'"
|
23
|
+
ret = r.result.size != 0
|
24
|
+
r.clear
|
25
|
+
ret
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the PostgreSQL OID of a table within the database or
|
29
|
+
# nil if it doesn't exist. Mostly for internal usage.
|
30
|
+
|
31
|
+
def table_oid(table)
|
32
|
+
r = self.exec "SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) AND c.relname='#{self.class.escape(table.to_s)}'"
|
33
|
+
ret = r.result.flatten.first
|
34
|
+
r.clear
|
35
|
+
ret
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an array of arrays containing the list of fields within a
|
39
|
+
# table. Each element contains two elements, the first is the field
|
40
|
+
# name and the second is the field type. Returns nil if the table
|
41
|
+
# does not exist.
|
42
|
+
|
43
|
+
def table_field_list(table)
|
44
|
+
return nil unless pg_oid = table_oid(table)
|
45
|
+
r = self.exec "SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod) FROM pg_catalog.pg_attribute a WHERE a.attrelid = '#{pg_oid}' AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum"
|
46
|
+
ret = r.result
|
47
|
+
r.clear
|
48
|
+
ret
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns an array of arrays containing the PostgreSQL foreign keys
|
52
|
+
# within a table. The first element is the constraint name and the
|
53
|
+
# second element is the constraint definition.
|
54
|
+
|
55
|
+
def table_foreign_keys(table)
|
56
|
+
return nil unless pg_oid = table_oid(table)
|
57
|
+
r = self.exec "SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_catalog.pg_constraint r WHERE r.conrelid = '#{pg_oid}' AND r.contype = 'f'"
|
58
|
+
ret = r.result
|
59
|
+
r.clear
|
60
|
+
ret
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
8
65
|
require 'og/store/sql'
|
9
66
|
|
10
67
|
# Customize the standard postgres resultset to make
|
@@ -71,6 +128,8 @@ end
|
|
71
128
|
# A Store that persists objects into a PostgreSQL database.
|
72
129
|
# To read documentation about the methods, consult the documentation
|
73
130
|
# for SqlStore and Store.
|
131
|
+
#
|
132
|
+
# This is the reference Og store.
|
74
133
|
#
|
75
134
|
# === Design
|
76
135
|
#
|
@@ -92,6 +151,28 @@ class PsqlStore < SqlStore
|
|
92
151
|
super
|
93
152
|
end
|
94
153
|
|
154
|
+
# Purges all tables from the database.
|
155
|
+
|
156
|
+
def self.destroy_tables(options)
|
157
|
+
|
158
|
+
conn = PGconn.connect(
|
159
|
+
options[:address],
|
160
|
+
options[:port], nil, nil,
|
161
|
+
options[:name],
|
162
|
+
options[:user].to_s,
|
163
|
+
options[:password].to_s
|
164
|
+
)
|
165
|
+
|
166
|
+
conn.list_tables.each do |table|
|
167
|
+
sql = "DROP TABLE #{table} CASCADE"
|
168
|
+
conn.exec sql
|
169
|
+
Logger.debug "Dropped database table #{table}"
|
170
|
+
end
|
171
|
+
|
172
|
+
conn.close
|
173
|
+
end
|
174
|
+
|
175
|
+
|
95
176
|
def initialize(options)
|
96
177
|
super
|
97
178
|
|
@@ -134,7 +215,7 @@ class PsqlStore < SqlStore
|
|
134
215
|
klass.const_set 'OGSEQ', "#{table(klass)}_oid_seq"
|
135
216
|
end
|
136
217
|
|
137
|
-
if klass.ann.
|
218
|
+
if klass.ann.self.primary_key.symbol == :oid
|
138
219
|
unless klass.properties.include? :oid
|
139
220
|
klass.property :oid, Fixnum, :sql => 'serial PRIMARY KEY'
|
140
221
|
end
|
@@ -165,7 +246,7 @@ class PsqlStore < SqlStore
|
|
165
246
|
end
|
166
247
|
|
167
248
|
# Start a new transaction.
|
168
|
-
|
249
|
+
|
169
250
|
def start
|
170
251
|
# neumann: works with earlier PSQL databases too.
|
171
252
|
exec('BEGIN TRANSACTION') if @transaction_nesting < 1
|
@@ -174,79 +255,166 @@ class PsqlStore < SqlStore
|
|
174
255
|
|
175
256
|
private
|
176
257
|
|
177
|
-
|
178
|
-
|
258
|
+
# Adds foreign key constraints to a join table, replicating all
|
259
|
+
# modifications to OIDs to the join tables and also purging
|
260
|
+
# any left over data from deleted records (at the time of
|
261
|
+
# implementation, self-join cases left this data here).
|
179
262
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
263
|
+
def create_join_table_foreign_key_constraints(klass,info)
|
264
|
+
|
265
|
+
table_list = @conn.list_tables
|
266
|
+
|
267
|
+
needed_tables = [ info[:table], info[:first_table], info[:second_table] ]
|
268
|
+
missing_tables = Array.new
|
269
|
+
needed_tables.each do |table|
|
270
|
+
missing_tables << table unless table_list.include?(table)
|
186
271
|
end
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
for data in indices
|
194
|
-
idx, options = *data
|
195
|
-
idx = idx.to_s
|
196
|
-
pre_sql, post_sql = options[:pre], options[:post]
|
197
|
-
idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
|
198
|
-
sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
|
272
|
+
if missing_tables.size > 0
|
273
|
+
msg = "Join table #{info[:table]} needs PostgreSQL foreign key constraints but the following table"
|
274
|
+
msg << (missing_tables.size > 1 ? "s were " : " was ")
|
275
|
+
msg << "missing: "
|
276
|
+
missing_tables.each do |table|
|
277
|
+
msg << "#{table}, "
|
199
278
|
end
|
279
|
+
Logger.debug msg[0..-3] + ". (Should be retried later)."
|
280
|
+
return false
|
200
281
|
end
|
201
282
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
283
|
+
#This info should maybe be in join metadata?
|
284
|
+
target_class = nil
|
285
|
+
klass.relations.each do |rel|
|
286
|
+
if rel.join_table == info[:table]
|
287
|
+
target_class = rel.target_class
|
288
|
+
break
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
if target_class
|
293
|
+
Logger.debug "Adding PostgreSQL foreign key constraints to #{info[:table]}"
|
294
|
+
|
295
|
+
# TODO: This should also interrograte the constraint definition
|
296
|
+
# incase people meddle with the database in an insane fashion
|
297
|
+
# (very, very low priority)
|
298
|
+
|
299
|
+
existing_constraints = @conn.table_foreign_keys(info[:table]).map {|fk| fk.first}
|
300
|
+
|
301
|
+
constraints = Array.new
|
302
|
+
constraints << { :table => info[:first_table], :join_table => info[:table], :primary_key => klass.primary_key.field || klass.primary_key.symbol, :foreign_key => info[:first_key] }
|
303
|
+
constraints << { :table => info[:second_table], :join_table => info[:table], :primary_key => target_class.primary_key.field || target_class.primary_key.symbol, :foreign_key => info[:second_key] }
|
304
|
+
|
305
|
+
constraints.each do |constraint|
|
306
|
+
constraint_name = "ogc_#{constraint[:table]}_#{constraint[:foreign_key]}"
|
307
|
+
if existing_constraints.include?(constraint_name)
|
308
|
+
Logger.debug "PostgreSQL foreign key constraint linking #{constraint[:foreign_key]} on #{constraint[:join_table]} to #{constraint[:primary_key]} on #{constraint[:table]} already exists (#{constraint_name})."
|
309
|
+
next
|
310
|
+
end
|
311
|
+
sql = "ALTER TABLE #{constraint[:join_table]} ADD CONSTRAINT #{constraint_name} FOREIGN KEY (#{constraint[:foreign_key]}) REFERENCES #{constraint[:table]} (#{constraint[:primary_key]}) ON UPDATE CASCADE ON DELETE CASCADE"
|
312
|
+
@conn.exec(sql).clear
|
313
|
+
Logger.debug "Added PostgreSQL foreign key constraint linking #{constraint[:foreign_key]} on #{constraint[:join_table]} to #{constraint[:primary_key]} on #{constraint[:table]} (#{constraint_name})."
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def create_table(klass)
|
319
|
+
fields = fields_for_class(klass)
|
320
|
+
|
321
|
+
unless @conn.table_exists? klass::OGTABLE
|
322
|
+
|
323
|
+
sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
|
324
|
+
|
325
|
+
# Create table constraints.
|
326
|
+
|
327
|
+
if constraints = klass.ann.self[:sql_constraint]
|
328
|
+
sql << ", #{constraints.join(', ')}"
|
329
|
+
end
|
330
|
+
|
331
|
+
sql << ") WITHOUT OIDS;"
|
332
|
+
|
333
|
+
# Create indices.
|
334
|
+
|
335
|
+
if indices = klass.ann.self[:index]
|
336
|
+
for data in indices
|
337
|
+
idx, options = *data
|
338
|
+
idx = idx.to_s
|
339
|
+
pre_sql, post_sql = options[:pre], options[:post]
|
340
|
+
idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
|
341
|
+
sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
@conn.exec(sql).clear
|
346
|
+
Logger.info "Created table '#{klass::OGTABLE}'."
|
347
|
+
else
|
348
|
+
Logger.debug "Table #{klass::OGTABLE} already exists"
|
349
|
+
#rp: basic field interrogation
|
350
|
+
# TODO: Add type checking.
|
351
|
+
|
352
|
+
actual_fields = @conn.table_field_list(klass::OGTABLE).map {|pair| pair.first}
|
353
|
+
|
354
|
+
#Make new ones always - don't destroy by default because it might contain data you want back.
|
355
|
+
need_fields = fields.each do |needed_field|
|
356
|
+
field_name = needed_field[0..(needed_field.index(' ')-1)]
|
357
|
+
next if actual_fields.include?(field_name)
|
358
|
+
|
359
|
+
if @options[:evolve_schema] == true
|
360
|
+
Logger.debug "Adding field '#{needed_field}' to '#{klass::OGTABLE}'"
|
361
|
+
sql = "ALTER TABLE #{klass::OGTABLE} ADD COLUMN #{needed_field}"
|
362
|
+
@conn.exec(sql)
|
363
|
+
else
|
364
|
+
Logger.info "WARNING: Table '#{klass::OGTABLE}' is missing field '#{needed_field}' and :evolve_schema is not set to true!"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
#Drop old ones
|
369
|
+
needed_fields = fields.map {|f| f =~ /^([^ ]+)/; $1}
|
370
|
+
actual_fields.each do |obsolete_field|
|
371
|
+
next if needed_fields.include?(obsolete_field)
|
372
|
+
if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false
|
373
|
+
sql = "ALTER TABLE #{klass::OGTABLE} DROP COLUMN #{obsolete_field}"
|
374
|
+
Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass::OGTABLE}'"
|
375
|
+
@conn.exec(sql)
|
376
|
+
else
|
377
|
+
Logger.info "WARNING: You have an obsolete field '#{obsolete_field}' on table '#{klass::OGTABLE}' and :evolve_schema is not set or is in cautious mode!"
|
378
|
+
end
|
212
379
|
end
|
213
380
|
end
|
214
381
|
|
215
382
|
# Create join tables if needed. Join tables are used in
|
216
383
|
# 'many_to_many' relations.
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
if ex.to_s =~ /relation .* already exists/i
|
228
|
-
Logger.debug 'Join table already exists' if $DBG
|
384
|
+
players = klass.resolve_remote_relations.map{|rel| rel.owner_class if rel.collection}.compact.uniq
|
385
|
+
players << klass
|
386
|
+
players.each do |player|
|
387
|
+
if join_tables = player.ann.self[:join_tables]
|
388
|
+
for info in join_tables
|
389
|
+
unless @conn.table_exists? info[:table]
|
390
|
+
create_join_table_sql(info).each do |sql|
|
391
|
+
@conn.exec(sql).clear
|
392
|
+
end
|
393
|
+
Logger.debug "Created jointable '#{info[:table]}'."
|
229
394
|
else
|
230
|
-
|
395
|
+
Logger.debug "Join table '#{info[:table]}' already exists."
|
231
396
|
end
|
397
|
+
create_join_table_foreign_key_constraints(player,info)
|
232
398
|
end
|
233
399
|
end
|
234
400
|
end
|
401
|
+
|
235
402
|
end
|
236
403
|
|
237
404
|
def drop_table(klass)
|
238
|
-
|
239
|
-
|
405
|
+
# foreign key constraints will remove the need to do manual cleanup on
|
406
|
+
# postgresql join tables.
|
407
|
+
exec "DROP TABLE #{klass.table} CASCADE"
|
240
408
|
end
|
241
409
|
|
242
410
|
def create_field_map(klass)
|
243
411
|
res = @conn.exec "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
|
244
412
|
map = {}
|
245
|
-
|
413
|
+
|
246
414
|
for field in res.fields
|
247
415
|
map[field.intern] = res.fieldnum(field)
|
248
416
|
end
|
249
|
-
|
417
|
+
|
250
418
|
return map
|
251
419
|
ensure
|
252
420
|
res.clear if res
|
@@ -284,23 +452,23 @@ private
|
|
284
452
|
props << Property.new(:symbol => :ogtype, :klass => String)
|
285
453
|
values << ", '#{klass}'"
|
286
454
|
end
|
287
|
-
|
455
|
+
|
288
456
|
sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| field_for_property(p)}.join(',')}) VALUES (#{values})"
|
289
457
|
|
290
458
|
klass.class_eval %{
|
291
459
|
def og_insert(store)
|
292
|
-
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
460
|
+
#{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
293
461
|
res = store.conn.exec "SELECT nextval('#{klass::OGSEQ}')"
|
294
462
|
@#{klass.pk_symbol} = res.getvalue(0, 0).to_i
|
295
463
|
res.clear
|
296
464
|
store.conn.exec("#{sql}").clear
|
297
|
-
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
465
|
+
#{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
298
466
|
end
|
299
467
|
}
|
300
468
|
end
|
301
469
|
|
302
470
|
def eval_og_allocate(klass)
|
303
|
-
if klass.ann.
|
471
|
+
if klass.ann.self[:subclasses]
|
304
472
|
klass.module_eval %{
|
305
473
|
def self.og_allocate(res, row = 0)
|
306
474
|
Object.constant(res.getvalue(row, 0)).allocate
|
@@ -328,3 +496,4 @@ end
|
|
328
496
|
# * George Moschovitis <gm@navel.gr>
|
329
497
|
# * Michael Neumann <mneumann@ntecs.de>
|
330
498
|
# * Ysabel <deb@ysabel.org>
|
499
|
+
# * Rob Pitt <rob@motionpath.com>
|
data/lib/og/store/sql.rb
CHANGED
@@ -8,7 +8,7 @@ module Og
|
|
8
8
|
module SqlUtils
|
9
9
|
|
10
10
|
# Escape an SQL string
|
11
|
-
|
11
|
+
|
12
12
|
def escape(str)
|
13
13
|
return nil unless str
|
14
14
|
return str.gsub(/'/, "''")
|
@@ -16,19 +16,19 @@ module SqlUtils
|
|
16
16
|
|
17
17
|
# Convert a ruby time to an sql timestamp.
|
18
18
|
#--
|
19
|
-
# TODO: Optimize this
|
19
|
+
# TODO: Optimize this.
|
20
20
|
#++
|
21
|
-
|
21
|
+
|
22
22
|
def timestamp(time = Time.now)
|
23
23
|
return nil unless time
|
24
24
|
return time.strftime("%Y-%m-%d %H:%M:%S")
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# Output YYY-mm-dd
|
28
28
|
#--
|
29
29
|
# TODO: Optimize this.
|
30
30
|
#++
|
31
|
-
|
31
|
+
|
32
32
|
def date(date)
|
33
33
|
return nil unless date
|
34
34
|
return "#{date.year}-#{date.month}-#{date.mday}"
|
@@ -37,40 +37,40 @@ module SqlUtils
|
|
37
37
|
#--
|
38
38
|
# TODO: implement me!
|
39
39
|
#++
|
40
|
-
|
40
|
+
|
41
41
|
def blob(val)
|
42
42
|
val
|
43
43
|
end
|
44
44
|
|
45
45
|
# Parse an integer.
|
46
|
-
|
46
|
+
|
47
47
|
def parse_int(int)
|
48
48
|
int = int.to_i if int
|
49
49
|
int
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
# Parse a float.
|
53
|
-
|
53
|
+
|
54
54
|
def parse_float(fl)
|
55
55
|
fl = fl.to_f if fl
|
56
56
|
fl
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
# Parse sql datetime
|
60
60
|
#--
|
61
61
|
# TODO: Optimize this.
|
62
62
|
#++
|
63
|
-
|
63
|
+
|
64
64
|
def parse_timestamp(str)
|
65
65
|
return nil unless str
|
66
66
|
return Time.parse(str)
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
# Input YYYY-mm-dd
|
70
70
|
#--
|
71
71
|
# TODO: Optimize this.
|
72
72
|
#++
|
73
|
-
|
73
|
+
|
74
74
|
def parse_date(str)
|
75
75
|
return nil unless str
|
76
76
|
return Date.strptime(str)
|
@@ -79,13 +79,13 @@ module SqlUtils
|
|
79
79
|
#--
|
80
80
|
# TODO: implement me!!
|
81
81
|
#++
|
82
|
-
|
82
|
+
|
83
83
|
def parse_blob(val)
|
84
84
|
val
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
# Escape the various Ruby types.
|
88
|
-
|
88
|
+
|
89
89
|
def quote(val)
|
90
90
|
case val
|
91
91
|
when Fixnum, Integer, Float
|
@@ -98,20 +98,20 @@ module SqlUtils
|
|
98
98
|
val ? "'#{date(val)}'" : 'NULL'
|
99
99
|
when TrueClass
|
100
100
|
val ? "'t'" : 'NULL'
|
101
|
-
else
|
101
|
+
else
|
102
102
|
# gmosx: keep the '' for nil symbols.
|
103
103
|
val ? escape(val.to_yaml) : ''
|
104
|
-
end
|
104
|
+
end
|
105
105
|
end
|
106
106
|
|
107
107
|
# Apply table name conventions to a class name.
|
108
|
-
|
108
|
+
|
109
109
|
def tableize(klass)
|
110
110
|
"#{klass.to_s.gsub(/::/, "_").downcase}"
|
111
111
|
end
|
112
112
|
|
113
113
|
def table(klass)
|
114
|
-
klass.ann.
|
114
|
+
klass.ann.self[:sql_table] || klass.ann.self[:table] || "#{Og.table_prefix}#{tableize(klass)}"
|
115
115
|
end
|
116
116
|
|
117
117
|
def join_object_ordering(obj1, obj2)
|
@@ -129,7 +129,7 @@ module SqlUtils
|
|
129
129
|
return class2, class1, true
|
130
130
|
end
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
133
|
def build_join_name(class1, class2, postfix = nil)
|
134
134
|
# Don't reorder arguments, as this is used in places that
|
135
135
|
# have already determined the order they want.
|
@@ -149,14 +149,14 @@ module SqlUtils
|
|
149
149
|
klass = klass.schema_inheritance_root_class if klass.schema_inheritance_child?
|
150
150
|
"#{klass.to_s.split('::').last.downcase}_oid"
|
151
151
|
end
|
152
|
-
|
152
|
+
|
153
153
|
def join_table_keys(class1, class2)
|
154
154
|
if class1 == class2
|
155
155
|
# Fix for the self-join case.
|
156
156
|
return join_table_key(class1), "#{join_table_key(class2)}2"
|
157
157
|
else
|
158
158
|
return join_table_key(class1), join_table_key(class2)
|
159
|
-
end
|
159
|
+
end
|
160
160
|
end
|
161
161
|
|
162
162
|
def ordered_join_table_keys(class1, class2)
|
@@ -165,7 +165,7 @@ module SqlUtils
|
|
165
165
|
end
|
166
166
|
|
167
167
|
def join_table_info(owner_class, target_class, postfix = nil)
|
168
|
-
|
168
|
+
|
169
169
|
# some fixes for schema inheritance.
|
170
170
|
|
171
171
|
owner_class = owner_class.schema_inheritance_root_class if owner_class.schema_inheritance_child?
|
@@ -173,13 +173,13 @@ module SqlUtils
|
|
173
173
|
|
174
174
|
owner_key, target_key = join_table_keys(owner_class, target_class)
|
175
175
|
first, second, changed = join_class_ordering(owner_class, target_class)
|
176
|
-
|
176
|
+
|
177
177
|
if changed
|
178
178
|
first_key, second_key = target_key, owner_key
|
179
179
|
else
|
180
180
|
first_key, second_key = owner_key, target_key
|
181
181
|
end
|
182
|
-
|
182
|
+
|
183
183
|
return {
|
184
184
|
:table => join_table(owner_class, target_class, postfix),
|
185
185
|
:owner_key => owner_key,
|
@@ -195,7 +195,7 @@ module SqlUtils
|
|
195
195
|
|
196
196
|
# Subclasses can override this if they need a different
|
197
197
|
# syntax.
|
198
|
-
|
198
|
+
|
199
199
|
def create_join_table_sql(join_table_info, suffix = 'NOT NULL', key_type = 'integer')
|
200
200
|
join_table = join_table_info[:table]
|
201
201
|
first_index = join_table_info[:first_index]
|
@@ -210,13 +210,13 @@ module SqlUtils
|
|
210
210
|
#{first_key} integer NOT NULL,
|
211
211
|
#{second_key} integer NOT NULL,
|
212
212
|
PRIMARY KEY(#{first_key}, #{second_key})
|
213
|
-
)
|
213
|
+
)
|
214
214
|
}
|
215
|
-
|
215
|
+
|
216
216
|
# gmosx: not that useful?
|
217
217
|
# sql << "CREATE INDEX #{first_index} ON #{join_table} (#{first_key})"
|
218
218
|
# sql << "CREATE INDEX #{second_index} ON #{join_table} (#{second_key})"
|
219
|
-
|
219
|
+
|
220
220
|
return sql
|
221
221
|
end
|
222
222
|
|
@@ -237,7 +237,7 @@ class SqlStore < Store
|
|
237
237
|
|
238
238
|
# The default Ruby <-> SQL type mappings, should be valid
|
239
239
|
# for most RDBM systems.
|
240
|
-
|
240
|
+
|
241
241
|
@typemap = {
|
242
242
|
Integer => 'integer',
|
243
243
|
Fixnum => 'integer',
|
@@ -265,12 +265,36 @@ class SqlStore < Store
|
|
265
265
|
Glue::Aspects.wrap(klass, [:exec, :query])
|
266
266
|
end
|
267
267
|
|
268
|
-
#
|
268
|
+
# Returns a list of tables that exist within the database but are
|
269
|
+
# not managed by the supplied manager.
|
270
|
+
|
271
|
+
def unmanaged_tables(manager)
|
272
|
+
ret = Array.new
|
273
|
+
mt = managed_tables(manager)
|
274
|
+
@conn.list_tables.each do |table|
|
275
|
+
ret << table unless mt.include?(table)
|
276
|
+
end
|
277
|
+
ret
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns a list of tables within the database that are there to
|
281
|
+
# support a class managed by the supplied manager.
|
282
|
+
|
283
|
+
def managed_tables(manager)
|
284
|
+
ret = Array.new
|
285
|
+
manager.managed_classes.each do |klass|
|
286
|
+
ret << klass::OGTABLE
|
287
|
+
ret.concat(klass.relations.reject{|rel| not rel.options[:join_table]}.map{|rel| rel.options[:join_table]})
|
288
|
+
end
|
289
|
+
ret
|
290
|
+
end
|
291
|
+
|
292
|
+
# Enchants a class.
|
269
293
|
|
270
294
|
def enchant(klass, manager)
|
271
295
|
|
272
296
|
# setup the table where this class is mapped.
|
273
|
-
|
297
|
+
|
274
298
|
if klass.schema_inheritance_child?
|
275
299
|
# farms: allow deeper inheritance (TODO: use annotation :superclass)
|
276
300
|
klass.const_set 'OGTABLE', table(klass.schema_inheritance_root_class)
|
@@ -279,13 +303,13 @@ class SqlStore < Store
|
|
279
303
|
end
|
280
304
|
|
281
305
|
klass.module_eval 'def self.table; OGTABLE; end'
|
282
|
-
|
306
|
+
|
283
307
|
eval_og_allocate(klass)
|
284
|
-
|
308
|
+
|
285
309
|
super
|
286
310
|
|
287
311
|
unless klass.polymorphic_parent?
|
288
|
-
# precompile class specific lifecycle methods.
|
312
|
+
# precompile class specific lifecycle methods.
|
289
313
|
eval_og_create_schema(klass)
|
290
314
|
eval_og_insert(klass)
|
291
315
|
eval_og_update(klass)
|
@@ -309,7 +333,9 @@ class SqlStore < Store
|
|
309
333
|
# Loads an object from the store using the primary key.
|
310
334
|
|
311
335
|
def load(pk, klass)
|
312
|
-
|
336
|
+
sql = "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
|
337
|
+
sql << " AND ogtype='#{klass}'" if klass.schema_inheritance_child?
|
338
|
+
res = query sql
|
313
339
|
read_one(res, klass)
|
314
340
|
end
|
315
341
|
alias_method :exist?, :load
|
@@ -318,7 +344,9 @@ class SqlStore < Store
|
|
318
344
|
|
319
345
|
def reload(obj, pk)
|
320
346
|
raise 'Cannot reload unmanaged object' unless obj.saved?
|
321
|
-
|
347
|
+
sql = "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
|
348
|
+
sql << " AND ogtype='#{obj.class}'" if obj.class.schema_inheritance_child?
|
349
|
+
res = query sql
|
322
350
|
obj.og_read(res.next, 0)
|
323
351
|
ensure
|
324
352
|
res.close if res
|
@@ -330,7 +358,7 @@ class SqlStore < Store
|
|
330
358
|
#--
|
331
359
|
# gmosx, THINK: condition is not really useful here :(
|
332
360
|
#++
|
333
|
-
|
361
|
+
|
334
362
|
def update(obj, options = nil)
|
335
363
|
if options and properties = options[:only]
|
336
364
|
if properties.is_a?(Array)
|
@@ -352,7 +380,7 @@ class SqlStore < Store
|
|
352
380
|
|
353
381
|
# Update selected properties of an object or class of
|
354
382
|
# objects.
|
355
|
-
|
383
|
+
|
356
384
|
def update_properties(target, *properties)
|
357
385
|
update(target, :only => properties)
|
358
386
|
end
|
@@ -360,10 +388,10 @@ class SqlStore < Store
|
|
360
388
|
alias_method :update_property, :update_properties
|
361
389
|
|
362
390
|
# More generalized method, also allows for batch updates.
|
363
|
-
|
391
|
+
|
364
392
|
def update_by_sql(target, set, options = nil)
|
365
393
|
set = set.gsub(/@/, '')
|
366
|
-
|
394
|
+
|
367
395
|
if target.is_a?(Class)
|
368
396
|
sql = "UPDATE #{target.table} SET #{set} "
|
369
397
|
sql << " WHERE #{options[:condition]}" if options and options[:condition]
|
@@ -381,7 +409,7 @@ class SqlStore < Store
|
|
381
409
|
#
|
382
410
|
# User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
|
383
411
|
# Comment.find(:include => :entry)
|
384
|
-
|
412
|
+
|
385
413
|
def find(options)
|
386
414
|
klass = options[:class]
|
387
415
|
sql = resolve_options(klass, options)
|
@@ -389,7 +417,7 @@ class SqlStore < Store
|
|
389
417
|
end
|
390
418
|
|
391
419
|
# Find one object.
|
392
|
-
|
420
|
+
|
393
421
|
def find_one(options)
|
394
422
|
klass = options[:class]
|
395
423
|
# gmosx, THINK: should not set this by default.
|
@@ -400,15 +428,15 @@ class SqlStore < Store
|
|
400
428
|
|
401
429
|
# Perform a custom sql query and deserialize the
|
402
430
|
# results.
|
403
|
-
|
431
|
+
|
404
432
|
def select(sql, klass)
|
405
433
|
sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
|
406
434
|
read_all(query(sql), klass)
|
407
435
|
end
|
408
436
|
alias_method :find_by_sql, :select
|
409
|
-
|
437
|
+
|
410
438
|
# Specialized one result version of select.
|
411
|
-
|
439
|
+
|
412
440
|
def select_one(sql, klass)
|
413
441
|
sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
|
414
442
|
read_one(query(sql), klass)
|
@@ -416,7 +444,7 @@ class SqlStore < Store
|
|
416
444
|
alias_method :find_by_sql_one, :select_one
|
417
445
|
|
418
446
|
# Perform an aggregation over query results.
|
419
|
-
|
447
|
+
|
420
448
|
def aggregate(options)
|
421
449
|
if options.is_a?(String)
|
422
450
|
sql = options
|
@@ -425,16 +453,20 @@ class SqlStore < Store
|
|
425
453
|
sql = "SELECT #{aggregate} FROM #{options[:class].table}"
|
426
454
|
if condition = options[:condition]
|
427
455
|
sql << " WHERE #{condition}"
|
456
|
+
sql << " AND " if options[:class].schema_inheritance_child?
|
457
|
+
else
|
458
|
+
sql << " WHERE " if options[:class].schema_inheritance_child?
|
428
459
|
end
|
429
|
-
|
430
|
-
|
460
|
+
sql << "ogtype='#{options[:class]}'" if options[:class].schema_inheritance_child?
|
461
|
+
end
|
462
|
+
|
431
463
|
query(sql).first_value.to_i
|
432
464
|
end
|
433
465
|
alias_method :count, :aggregate
|
434
|
-
|
466
|
+
|
435
467
|
# Relate two objects through an intermediate join table.
|
436
468
|
# Typically used in joins_many and many_to_many relations.
|
437
|
-
|
469
|
+
|
438
470
|
def join(obj1, obj2, table, options = nil)
|
439
471
|
first, second = join_object_ordering(obj1, obj2)
|
440
472
|
first_key, second_key = ordered_join_table_keys(obj1.class, obj2.class)
|
@@ -443,48 +475,48 @@ class SqlStore < Store
|
|
443
475
|
else
|
444
476
|
exec "INSERT INTO #{table} (#{first_key},#{second_key}) VALUES (#{first.pk}, #{second.pk})"
|
445
477
|
end
|
446
|
-
end
|
478
|
+
end
|
447
479
|
|
448
480
|
# Unrelate two objects be removing their relation from the
|
449
481
|
# join table.
|
450
|
-
|
482
|
+
|
451
483
|
def unjoin(obj1, obj2, table)
|
452
484
|
first, second = join_object_ordering(obj1, obj2)
|
453
485
|
first_key, second_key = ordered_join_table_keys(obj1.class, obj2.class)
|
454
486
|
exec "DELETE FROM #{table} WHERE #{first_key}=#{first.pk} AND #{second_key}=#{second.pk}"
|
455
487
|
end
|
456
|
-
|
488
|
+
|
457
489
|
def delete_all(klass)
|
458
490
|
exec "DELETE FROM #{klass.table}"
|
459
491
|
end
|
460
|
-
|
492
|
+
|
461
493
|
# :section: Transaction methods.
|
462
|
-
|
494
|
+
|
463
495
|
# Start a new transaction.
|
464
|
-
|
496
|
+
|
465
497
|
def start
|
466
498
|
exec('START TRANSACTION') if @transaction_nesting < 1
|
467
499
|
@transaction_nesting += 1
|
468
500
|
end
|
469
|
-
|
501
|
+
|
470
502
|
# Commit a transaction.
|
471
|
-
|
503
|
+
|
472
504
|
def commit
|
473
505
|
@transaction_nesting -= 1
|
474
506
|
exec('COMMIT') if @transaction_nesting < 1
|
475
507
|
end
|
476
|
-
|
508
|
+
|
477
509
|
# Rollback a transaction.
|
478
|
-
|
510
|
+
|
479
511
|
def rollback
|
480
512
|
@transaction_nesting -= 1
|
481
513
|
exec('ROLLBACK') if @transaction_nesting < 1
|
482
514
|
end
|
483
515
|
|
484
516
|
# :section: Low level methods.
|
485
|
-
|
517
|
+
|
486
518
|
# Encapsulates a low level update method.
|
487
|
-
|
519
|
+
|
488
520
|
def sql_update(sql)
|
489
521
|
exec(sql)
|
490
522
|
# return affected rows.
|
@@ -503,6 +535,13 @@ private
|
|
503
535
|
# persisted.
|
504
536
|
|
505
537
|
def drop_table(klass)
|
538
|
+
# Remove leftover data from some join tabkes.
|
539
|
+
klass.relations.each do |rel|
|
540
|
+
if rel.class.to_s == "Og::JoinsMany" and rel.join_table
|
541
|
+
target_class = rel.target_class
|
542
|
+
exec "DELETE FROM #{rel.join_table}"
|
543
|
+
end
|
544
|
+
end
|
506
545
|
exec "DROP TABLE #{klass.table}"
|
507
546
|
end
|
508
547
|
alias_method :destroy, :drop_table
|
@@ -632,7 +671,7 @@ private
|
|
632
671
|
props = klass.properties.values.dup
|
633
672
|
values = props.collect { |p| write_prop(p) }.join(',')
|
634
673
|
|
635
|
-
if klass.ann.
|
674
|
+
if klass.ann.self[:superclass] or klass.ann.self[:subclasses]
|
636
675
|
props << Property.new(:symbol => :ogtype, :klass => String)
|
637
676
|
values << ", '#{klass}'"
|
638
677
|
end
|
@@ -641,9 +680,9 @@ private
|
|
641
680
|
|
642
681
|
klass.module_eval %{
|
643
682
|
def og_insert(store)
|
644
|
-
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
683
|
+
#{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
645
684
|
store.exec "#{sql}"
|
646
|
-
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
685
|
+
#{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
647
686
|
end
|
648
687
|
}
|
649
688
|
end
|
@@ -662,11 +701,11 @@ private
|
|
662
701
|
|
663
702
|
klass.module_eval %{
|
664
703
|
def og_update(store, options = nil)
|
665
|
-
#{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
|
704
|
+
#{Glue::Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
|
666
705
|
sql = "#{sql}"
|
667
706
|
sql << " AND \#{options[:condition]}" if options and options[:condition]
|
668
707
|
changed = store.sql_update(sql)
|
669
|
-
#{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
|
708
|
+
#{Glue::Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
|
670
709
|
return changed
|
671
710
|
end
|
672
711
|
}
|
@@ -684,57 +723,57 @@ private
|
|
684
723
|
|
685
724
|
for p in props
|
686
725
|
f = field_for_property(p).to_sym
|
687
|
-
|
726
|
+
|
688
727
|
if col = field_map[f]
|
689
728
|
code << "@#{p} = #{read_prop(p, col)}"
|
690
729
|
end
|
691
730
|
end
|
692
|
-
|
731
|
+
|
693
732
|
code = code.join('; ')
|
694
733
|
|
695
734
|
klass.module_eval %{
|
696
735
|
def og_read(res, row = 0, offset = 0)
|
697
|
-
#{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
|
736
|
+
#{Glue::Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
|
698
737
|
#{code}
|
699
|
-
#{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
|
738
|
+
#{Glue::Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
|
700
739
|
end
|
701
740
|
}
|
702
|
-
end
|
741
|
+
end
|
703
742
|
|
704
743
|
#--
|
705
744
|
# FIXME: is pk needed as parameter?
|
706
745
|
#++
|
707
|
-
|
746
|
+
|
708
747
|
def eval_og_delete(klass)
|
709
748
|
klass.module_eval %{
|
710
749
|
def og_delete(store, pk, cascade = true)
|
711
|
-
#{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
|
750
|
+
#{Glue::Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
|
712
751
|
pk ||= @#{klass.pk_symbol}
|
713
752
|
transaction do |tx|
|
714
753
|
tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
|
715
|
-
if cascade and #{klass}.ann.
|
716
|
-
#{klass}.ann.
|
754
|
+
if cascade and #{klass}.ann.self[:descendants]
|
755
|
+
#{klass}.ann.self.descendants.each do |dclass, foreign_key|
|
717
756
|
tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
|
718
757
|
end
|
719
758
|
end
|
720
759
|
end
|
721
|
-
#{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
|
760
|
+
#{Glue::Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
|
722
761
|
end
|
723
|
-
}
|
762
|
+
}
|
724
763
|
end
|
725
764
|
|
726
765
|
# Creates the schema for this class. Can be intercepted with
|
727
766
|
# aspects to add special behaviours.
|
728
|
-
|
767
|
+
|
729
768
|
def eval_og_create_schema(klass)
|
730
769
|
klass.module_eval %{
|
731
770
|
def og_create_schema(store)
|
732
771
|
if Og.create_schema
|
733
|
-
#{Aspects.gen_advice_code(:og_create_schema, klass.advices, :pre) if klass.respond_to?(:advices)}
|
772
|
+
#{Glue::Aspects.gen_advice_code(:og_create_schema, klass.advices, :pre) if klass.respond_to?(:advices)}
|
734
773
|
unless self.class.superclass.ancestors.include? SchemaInheritanceBase
|
735
774
|
store.send(:create_table, #{klass})
|
736
775
|
end
|
737
|
-
#{Aspects.gen_advice_code(:og_create_schema, klass.advices, :post) if klass.respond_to?(:advices)}
|
776
|
+
#{Glue::Aspects.gen_advice_code(:og_create_schema, klass.advices, :post) if klass.respond_to?(:advices)}
|
738
777
|
end
|
739
778
|
end
|
740
779
|
}
|
@@ -758,7 +797,7 @@ private
|
|
758
797
|
}
|
759
798
|
end
|
760
799
|
end
|
761
|
-
|
800
|
+
|
762
801
|
# :section: Misc methods.
|
763
802
|
|
764
803
|
def handle_sql_exception(ex, sql = nil)
|
@@ -770,12 +809,26 @@ private
|
|
770
809
|
return nil
|
771
810
|
end
|
772
811
|
|
812
|
+
# Resolve the finder options. Also takes scope into account.
|
813
|
+
#--
|
814
|
+
# FIXME: cleanup/refactor.
|
815
|
+
#++
|
816
|
+
|
773
817
|
def resolve_options(klass, options)
|
818
|
+
# Factor in scope.
|
819
|
+
|
820
|
+
if scope = klass.get_scope
|
821
|
+
scope = scope.dup
|
822
|
+
scond = scope.delete(:condition)
|
823
|
+
scope.update(options)
|
824
|
+
options = scope
|
825
|
+
end
|
826
|
+
|
774
827
|
if sql = options[:sql]
|
775
828
|
sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
|
776
829
|
return sql
|
777
830
|
end
|
778
|
-
|
831
|
+
|
779
832
|
tables = [klass::OGTABLE]
|
780
833
|
|
781
834
|
if included = options[:include]
|
@@ -785,7 +838,7 @@ private
|
|
785
838
|
if rel = klass.relation(name)
|
786
839
|
target_table = rel[:target_class]::OGTABLE
|
787
840
|
tables << target_table
|
788
|
-
|
841
|
+
|
789
842
|
if rel.is_a?(JoinsMany)
|
790
843
|
tables << rel[:join_table]
|
791
844
|
owner_key, target_key = klass.ogmanager.store.join_table_keys(klass, rel[:target_class])
|
@@ -813,16 +866,21 @@ private
|
|
813
866
|
update_condition options, options[:join_condition]
|
814
867
|
end
|
815
868
|
|
816
|
-
|
869
|
+
# Factor in scope in the conditions.
|
870
|
+
|
871
|
+
update_condition(options, scond) if scond
|
872
|
+
|
873
|
+
# rp: type is not set in all instances such as Class.first so this fix goes here for now.
|
874
|
+
if ogtype = options[:type] || (klass.schema_inheritance_child? ? "#{klass}" : nil)
|
817
875
|
update_condition options, "ogtype='#{ogtype}'"
|
818
876
|
end
|
819
877
|
|
820
878
|
sql = "SELECT #{fields} FROM #{tables.join(',')}"
|
821
|
-
|
879
|
+
|
822
880
|
if condition = options[:condition] || options[:where]
|
823
881
|
sql << " WHERE #{condition}"
|
824
882
|
end
|
825
|
-
|
883
|
+
|
826
884
|
if order = options[:order]
|
827
885
|
sql << " ORDER BY #{order}"
|
828
886
|
end
|
@@ -831,10 +889,10 @@ private
|
|
831
889
|
|
832
890
|
if extra = options[:extra]
|
833
891
|
sql << " #{extra}"
|
834
|
-
end
|
835
|
-
|
892
|
+
end
|
893
|
+
|
836
894
|
return sql
|
837
|
-
end
|
895
|
+
end
|
838
896
|
|
839
897
|
# Subclasses can override this if they need some other order.
|
840
898
|
# This is needed because different backends require different
|
@@ -853,15 +911,15 @@ private
|
|
853
911
|
# :section: Deserialization methods.
|
854
912
|
|
855
913
|
# Read a field (column) from a result set row.
|
856
|
-
|
914
|
+
|
857
915
|
def read_field
|
858
916
|
end
|
859
|
-
|
917
|
+
|
860
918
|
# Dynamicaly deserializes a result set row into an object.
|
861
919
|
# Used for specialized queries or join queries. Please
|
862
920
|
# not that this deserialization method is slower than the
|
863
921
|
# precompiled og_read method.
|
864
|
-
|
922
|
+
|
865
923
|
def read_row(obj, res, res_row, row)
|
866
924
|
res.fields.each_with_index do |field, idx|
|
867
925
|
obj.instance_variable_set "@#{field}", res_row[idx]
|
@@ -869,10 +927,10 @@ private
|
|
869
927
|
end
|
870
928
|
|
871
929
|
# Deserialize the join relations.
|
872
|
-
|
930
|
+
|
873
931
|
def read_join_relations(obj, res_row, row, join_relations)
|
874
932
|
offset = obj.class.properties.size
|
875
|
-
|
933
|
+
|
876
934
|
for rel in join_relations
|
877
935
|
rel_obj = rel[:target_class].og_allocate(res_row, row)
|
878
936
|
rel_obj.og_read(res_row, row, offset)
|
@@ -882,7 +940,7 @@ private
|
|
882
940
|
end
|
883
941
|
|
884
942
|
# Deserialize one object from the ResultSet.
|
885
|
-
|
943
|
+
|
886
944
|
def read_one(res, klass, options = nil)
|
887
945
|
return nil if res.blank?
|
888
946
|
|
@@ -891,11 +949,12 @@ private
|
|
891
949
|
klass.relation(n)
|
892
950
|
end
|
893
951
|
end
|
894
|
-
|
952
|
+
|
895
953
|
res_row = res.next
|
896
|
-
|
954
|
+
# causes STI classes to come back as the correct child class if accessed from the superclass
|
955
|
+
klass = Og::Entity::entity_from_string(res_row.result.flatten[res_row.fieldnum('ogtype')]) if klass.schema_inheritance?
|
897
956
|
obj = klass.og_allocate(res_row, 0)
|
898
|
-
|
957
|
+
|
899
958
|
if options and options[:select]
|
900
959
|
read_row(obj, res, res_row, 0)
|
901
960
|
else
|
@@ -904,7 +963,7 @@ private
|
|
904
963
|
end
|
905
964
|
|
906
965
|
return obj
|
907
|
-
|
966
|
+
|
908
967
|
ensure
|
909
968
|
res.close
|
910
969
|
end
|
@@ -936,10 +995,10 @@ private
|
|
936
995
|
objects << obj
|
937
996
|
end
|
938
997
|
end
|
939
|
-
|
998
|
+
|
940
999
|
return objects
|
941
|
-
|
942
|
-
ensure
|
1000
|
+
|
1001
|
+
ensure
|
943
1002
|
res.close
|
944
1003
|
end
|
945
1004
|
|
@@ -962,3 +1021,4 @@ end
|
|
962
1021
|
# * Ghislain Mary
|
963
1022
|
# * Ysabel <deb@ysabel.org>
|
964
1023
|
# * Guillaume Pierronnet <guillaume.pierronnet@laposte.net>
|
1024
|
+
# * Rob Pitt <rob@motionpath.com>
|