og 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
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>