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.
Files changed (51) hide show
  1. data/ProjectInfo +2 -5
  2. data/README +2 -0
  3. data/doc/AUTHORS +4 -1
  4. data/doc/RELEASES +53 -0
  5. data/examples/run.rb +2 -2
  6. data/lib/{og/mixin → glue}/hierarchical.rb +19 -19
  7. data/lib/{og/mixin → glue}/optimistic_locking.rb +1 -1
  8. data/lib/glue/orderable.rb +235 -0
  9. data/lib/glue/revisable.rb +2 -0
  10. data/lib/glue/taggable.rb +176 -0
  11. data/lib/{og/mixin/taggable.rb → glue/taggable_old.rb} +6 -0
  12. data/lib/glue/timestamped.rb +37 -0
  13. data/lib/{og/mixin → glue}/tree.rb +3 -8
  14. data/lib/og.rb +21 -20
  15. data/lib/og/collection.rb +15 -1
  16. data/lib/og/entity.rb +256 -114
  17. data/lib/og/manager.rb +60 -27
  18. data/lib/og/{mixin/schema_inheritance_base.rb → markers.rb} +5 -2
  19. data/lib/og/relation.rb +70 -74
  20. data/lib/og/relation/belongs_to.rb +5 -3
  21. data/lib/og/relation/has_many.rb +1 -0
  22. data/lib/og/relation/joins_many.rb +5 -4
  23. data/lib/og/store.rb +25 -46
  24. data/lib/og/store/alpha/filesys.rb +1 -1
  25. data/lib/og/store/alpha/kirby.rb +30 -30
  26. data/lib/og/store/alpha/memory.rb +49 -49
  27. data/lib/og/store/alpha/sqlserver.rb +7 -7
  28. data/lib/og/store/kirby.rb +38 -38
  29. data/lib/og/store/mysql.rb +43 -43
  30. data/lib/og/store/psql.rb +222 -53
  31. data/lib/og/store/sql.rb +165 -105
  32. data/lib/og/store/sqlite.rb +29 -25
  33. data/lib/og/validation.rb +24 -14
  34. data/lib/{vendor → og/vendor}/README +0 -0
  35. data/lib/{vendor → og/vendor}/kbserver.rb +1 -1
  36. data/lib/{vendor → og/vendor}/kirbybase.rb +230 -79
  37. data/lib/{vendor → og/vendor}/mysql.rb +0 -0
  38. data/lib/{vendor → og/vendor}/mysql411.rb +0 -0
  39. data/test/og/mixin/tc_hierarchical.rb +1 -1
  40. data/test/og/mixin/tc_optimistic_locking.rb +1 -1
  41. data/test/og/mixin/tc_orderable.rb +1 -1
  42. data/test/og/mixin/tc_taggable.rb +2 -2
  43. data/test/og/mixin/tc_timestamped.rb +2 -2
  44. data/test/og/tc_finder.rb +33 -0
  45. data/test/og/tc_inheritance.rb +2 -2
  46. data/test/og/tc_scoped.rb +45 -0
  47. data/test/og/tc_store.rb +1 -7
  48. metadata +21 -18
  49. data/lib/og/mixin/orderable.rb +0 -174
  50. data/lib/og/mixin/revisable.rb +0 -0
  51. data/lib/og/mixin/timestamped.rb +0 -24
@@ -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.this.primary_key.symbol == :oid
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
- def create_table(klass)
178
- fields = fields_for_class(klass)
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
- sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
181
-
182
- # Create table constraints.
183
-
184
- if constraints = klass.ann.this[:sql_constraint]
185
- sql << ", #{constraints.join(', ')}"
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
- sql << ") WITHOUT OIDS;"
189
-
190
- # Create indices.
191
-
192
- if indices = klass.ann.this[:index]
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
- begin
203
- @conn.exec(sql).clear
204
- Logger.info "Created table '#{klass::OGTABLE}'."
205
- rescue Object => ex
206
- # gmosx: any idea how to better test this?
207
- if ex.to_s =~ /relation .* already exists/i
208
- Logger.debug 'Table already exists'
209
- return
210
- else
211
- raise
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
- if join_tables = klass.ann.this[:join_tables]
219
- for info in join_tables
220
- begin
221
- create_join_table_sql(info).each do |sql|
222
- @conn.exec(sql).clear
223
- end
224
- Logger.debug "Created jointable '#{info[:table]}'."
225
- rescue Object => ex
226
- # gmosx: any idea how to better test this?
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
- raise
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
- super
239
- exec "DROP SEQUENCE #{klass::OGSEQ}"
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.this[:subclasses]
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>
@@ -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.this[:sql_table] || klass.ann.this[:table] || "#{Og.table_prefix}#{tableize(klass)}"
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
- # Enchants a class.
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
- res = query "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
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
- res = query "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
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
- end
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.this[:superclass] or klass.ann.this[:subclasses]
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.this[:descendants]
716
- #{klass}.ann.this.descendants.each do |dclass, foreign_key|
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
- if ogtype = options[:type]
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>