og 0.25.0 → 0.26.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.
@@ -75,4 +75,4 @@ class Manager
75
75
 
76
76
  end
77
77
 
78
- end
78
+ end
@@ -129,6 +129,9 @@ class Manager
129
129
  end
130
130
 
131
131
  # Is this class manageable by Og?
132
+ #--
133
+ # FIXME: investigate this (polymorphic/unmanageable).
134
+ #++
132
135
 
133
136
  def manageable?(klass)
134
137
  klass.respond_to?(:properties) and (!klass.properties.empty?) # and klass.ann.self.polymorphic.nil?
@@ -217,6 +220,19 @@ class Manager
217
220
  end
218
221
  alias_method :unmanage_class, :unmanage_classes
219
222
 
223
+ # Allows functionality that requires a store is finalized
224
+ # to be implemented. A vastly superior method of constructing
225
+ # foreign key constraints is an example of functionality
226
+ # this provides. Currently only used by the PostgreSQL store.
227
+ # Another good use for this would be an alternate table
228
+ # and field creation routine, which could be much faster,
229
+ # something I intend to do to the PostgreSQL store if nobody
230
+ # has reasons for objecting.
231
+
232
+ def post_setup
233
+ store.post_setup if store.respond_to?(:post_setup)
234
+ end
235
+
220
236
  end
221
237
 
222
238
  end
@@ -93,6 +93,10 @@ class Relation
93
93
  def enchant
94
94
  end
95
95
 
96
+ def to_s
97
+ @options[:target_name]
98
+ end
99
+
96
100
  # Access the hash values as methods.
97
101
 
98
102
  def method_missing(sym, *args)
@@ -320,4 +324,4 @@ end
320
324
 
321
325
  end
322
326
 
323
- # * George Moschovitis <gm@navel.gr>
327
+ # * George Moschovitis <gm@navel.gr>
@@ -4,7 +4,13 @@ module Og
4
4
 
5
5
  class RefersTo < Relation
6
6
 
7
+ def self.foreign_key(rel)
8
+ "#{rel[:foreign_name] || rel[:target_singular_name]}_#{rel[:target_class].primary_key}"
9
+ end
10
+
7
11
  def enchant
12
+ raise "#{target_singular_name} in #{owner_class} refers to an undefined class" if target_class.nil?
13
+
8
14
  self[:foreign_key] = "#{foreign_name || target_singular_name}_#{target_class.primary_key}"
9
15
 
10
16
  if self[:field]
@@ -17,6 +17,8 @@ class Store
17
17
  # Return the store for the given name.
18
18
 
19
19
  def self.for_name(name)
20
+ Logger.info "Og uses the #{name.to_s.capitalize} store."
21
+
20
22
  # gmosx: to keep RDoc happy.
21
23
  eval %{
22
24
  require 'og/store/#{name}'
@@ -94,6 +96,8 @@ class Store
94
96
  # update if this is already inserted in the database.
95
97
 
96
98
  def save(obj, options = nil)
99
+ return false unless obj.valid?
100
+
97
101
  if obj.saved?
98
102
  obj.og_update(self, options)
99
103
  else
@@ -101,6 +105,15 @@ class Store
101
105
  end
102
106
  end
103
107
  alias_method :<<, :save
108
+ alias_method :validate_and_save, :save
109
+
110
+ def force_save!(obj, options)
111
+ if obj.saved?
112
+ obj.og_update(self, options)
113
+ else
114
+ obj.og_insert(self)
115
+ end
116
+ end
104
117
 
105
118
  # Insert an object in the store.
106
119
 
@@ -1,7 +1,7 @@
1
1
  begin
2
- require 'og/vendor/kirbybase'
2
+ require 'kirbybase'
3
3
  rescue Object => ex
4
- Logger.error 'KirbyBase is not installed!'
4
+ Logger.error "KirbyBase is not installed. Please run 'gem install KirbyBase'"
5
5
  Logger.error ex
6
6
  end
7
7
 
@@ -58,9 +58,9 @@ class KirbyStore < SqlStore
58
58
  end
59
59
 
60
60
  def enchant(klass, manager)
61
- klass.send :attr_accessor, :recno
62
- klass.send :alias_method, :oid, :recno
63
- klass.send :alias_method, :oid=, :recno=
61
+ klass.send :attr_accessor, :oid
62
+ klass.send :alias_method, :recno, :oid
63
+ klass.send :alias_method, :recno=, :oid=
64
64
 
65
65
  symbols = klass.properties.keys
66
66
 
@@ -128,6 +128,7 @@ class KirbyStore < SqlStore
128
128
  end
129
129
 
130
130
  if order = options[:order]
131
+ order = order.to_s
131
132
  desc = (order =~ /DESC/)
132
133
  order = order.gsub(/DESC/, '').gsub(/ASC/, '')
133
134
  eval "objects.sort { |x, y| x.#{order} <=> y.#{order} }"
@@ -133,7 +133,7 @@ class MysqlStore < SqlStore
133
133
  def initialize(options)
134
134
  super
135
135
 
136
- @typemap.update(TrueClass => 'tinyint')
136
+ @typemap.update(TrueClass => 'tinyint', Time => 'datetime')
137
137
 
138
138
  @conn = Mysql.connect(
139
139
  options[:address] || 'localhost',
@@ -224,10 +224,39 @@ private
224
224
  # used instead of catching database exceptions
225
225
  # for 'table exists'?
226
226
 
227
- return if @conn.list_tables.include?(klass::OGTABLE)
227
+ fields = fields_for_class(klass)
228
228
 
229
+ if @conn.list_tables.include?(klass::OGTABLE)
230
+ actual_fields = conn.list_fields(klass::OGTABLE).fetch_fields.map {|f| f.name }
229
231
 
230
- fields = fields_for_class(klass)
232
+ #Make new ones always - don't destroy by default because it might contain data you want back.
233
+ need_fields = fields.each do |needed_field|
234
+ field_name = needed_field[0..(needed_field.index(' ')-1)]
235
+ next if actual_fields.include?(field_name)
236
+
237
+ if @options[:evolve_schema] == true
238
+ Logger.debug "Adding field '#{needed_field}' to '#{klass::OGTABLE}'"
239
+ sql = "ALTER TABLE #{klass::OGTABLE} ADD COLUMN #{needed_field}"
240
+ @conn.query(sql)
241
+ else
242
+ Logger.info "WARNING: Table '#{klass::OGTABLE}' is missing field '#{needed_field}' and :evolve_schema is not set to true!"
243
+ end
244
+ end
245
+
246
+ #Drop old ones
247
+ needed_fields = fields.map {|f| f =~ /^([^ ]+)/; $1}
248
+ actual_fields.each do |obsolete_field|
249
+ next if needed_fields.include?(obsolete_field)
250
+ if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false
251
+ sql = "ALTER TABLE #{klass::OGTABLE} DROP COLUMN #{obsolete_field}"
252
+ Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass::OGTABLE}'"
253
+ @conn.query(sql)
254
+ else
255
+ 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!"
256
+ end
257
+ end
258
+ return
259
+ end
231
260
 
232
261
  sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
233
262
 
@@ -276,8 +305,8 @@ private
276
305
  for info in join_tables
277
306
  begin
278
307
  create_join_table_sql(info).each do |sql|
279
- @conn.query sql
280
- end
308
+ @conn.query sql
309
+ end
281
310
  Logger.debug "Created jointable '#{info[:table]}'."
282
311
  rescue => ex
283
312
  if ex.respond_to?(:errno) and ex.errno == 1050 # table already exists.
@@ -8,8 +8,13 @@ end
8
8
  class PGconn
9
9
  # Lists all the tables within the database.
10
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)"
11
+ def list_tables
12
+ begin
13
+ 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)"
14
+ rescue Exception
15
+ # Racing
16
+ return []
17
+ end
13
18
  ret = r.result.flatten
14
19
  r.clear
15
20
  ret
@@ -19,7 +24,11 @@ class PGconn
19
24
  # otherwise.
20
25
 
21
26
  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)}'"
27
+ begin
28
+ 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)}'"
29
+ rescue Exception
30
+ return false # Racing...
31
+ end
23
32
  ret = r.result.size != 0
24
33
  r.clear
25
34
  ret
@@ -29,7 +38,11 @@ class PGconn
29
38
  # nil if it doesn't exist. Mostly for internal usage.
30
39
 
31
40
  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)}'"
41
+ begin
42
+ 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)}'"
43
+ rescue Exception
44
+ return nil # Racing...
45
+ end
33
46
  ret = r.result.flatten.first
34
47
  r.clear
35
48
  ret
@@ -48,14 +61,53 @@ class PGconn
48
61
  ret
49
62
  end
50
63
 
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.
64
+ # Returns a hash containing the foreign key constrains within a table.
65
+ # The keys are constraint names and the values are the constraint
66
+ # definitions.
54
67
 
55
68
  def table_foreign_keys(table)
56
69
  return nil unless pg_oid = table_oid(table)
57
70
  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
71
+ res = r.result
72
+ ret = Hash.new
73
+ res.each do |double|
74
+ ret[double.first] = double.last
75
+ end
76
+ r.clear
77
+ ret
78
+ end
79
+
80
+ # Returns a hash keyed by table (as a string) with each value also
81
+ # being a hash keyed by the constraint name (as a string) and the
82
+ # value being a string that contains the constraint definition.
83
+
84
+ def all_foreign_keys
85
+ loop_counter = 0
86
+ loop_max = 5
87
+ begin
88
+ r = self.exec "SELECT c.relname,r.conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace JOIN pg_catalog.pg_constraint r ON r.conrelid = c.oid WHERE c.relkind='r' AND r.contype ='f' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid);"
89
+ rescue RuntimeError => ex
90
+ raise unless ex.message =~ /cache lookup failed for relation (\d+)/ # Racing
91
+ # damaged_relation = $1
92
+ # Logger.error "Got damage to #{damaged_relation}"
93
+ loop_counter += 1
94
+ if loop_counter > loop_max
95
+ Logger.error "PostgreSQL had more than #{loop_max} cache errors, your database is almost certainly corrupt as pg_class does not match the PostgreSQL cache. Either use pg_dump to save the data, re-create the database, let og rebuild the schema and use pg_restore to restore the data, or repair it by hand"
96
+ exit
97
+ end
98
+ Logger.error "There is a problem with PostgreSQL's internal cache, retrying... (#{loop_counter} of #{loop_max})"
99
+ # you have a horrible setup anyhow, and it allows your
100
+ # horrible setup to work (deleting tables so fast
101
+ # in parallel PostgreSQL's internal lookups fail)
102
+ sleep 2
103
+ retry
104
+ end
105
+ res = r.result
106
+ ret = Hash.new
107
+ res.each do |tripple|
108
+ ret[tripple.first] ||= Hash.new
109
+ ret[tripple[0]][tripple[1]] = tripple[2]
110
+ end
59
111
  r.clear
60
112
  ret
61
113
  end
@@ -98,7 +150,7 @@ module PsqlUtils
98
150
 
99
151
  def escape(str)
100
152
  return nil unless str
101
- return PGconn.escape(str)
153
+ return PGconn.escape(str.to_s)
102
154
  end
103
155
 
104
156
  # TODO, mneumann:
@@ -164,9 +216,17 @@ class PsqlStore < SqlStore
164
216
  )
165
217
 
166
218
  conn.list_tables.each do |table|
167
- sql = "DROP TABLE #{table} CASCADE"
168
- conn.exec sql
169
- Logger.debug "Dropped database table #{table}"
219
+ begin
220
+ conn.exec "DROP TABLE #{table} CASCADE"
221
+ Logger.debug "Dropped database table #{table}"
222
+ rescue RuntimeError => ex
223
+ catch :ok do # Racing
224
+ throw :ok if ex.message =~ /tuple concurrently updated/
225
+ throw :ok if ex.message =~ /does not exist/
226
+ throw :ok if ex.message =~ /cache lookup failed/
227
+ raise
228
+ end
229
+ end
170
230
  end
171
231
 
172
232
  conn.close
@@ -185,7 +245,6 @@ class PsqlStore < SqlStore
185
245
  options[:user].to_s,
186
246
  options[:password].to_s
187
247
  )
188
-
189
248
  schema_order = options[:schema_order]
190
249
  encoding = options[:encoding]
191
250
  min_messages = options[:min_messages]
@@ -252,68 +311,283 @@ class PsqlStore < SqlStore
252
311
  exec('BEGIN TRANSACTION') if @transaction_nesting < 1
253
312
  @transaction_nesting += 1
254
313
  end
314
+
315
+ # Returns the Og::Manager that owns this store.
316
+
317
+ def manager
318
+ manager = nil
319
+ ok = false
320
+ ObjectSpace.each_object(Og::Manager) do |manager|
321
+ if manager.store.__id__ == self.__id__
322
+ ok = true
323
+ break
324
+ end
325
+ end
326
+ raise RuntimeError, "#{self.class} could not find it's manager" unless ok
327
+ manager
328
+ end
329
+
330
+ # Returns an array containing the constraints needed for this relation.
331
+ # The array contains hashes with the format:
332
+ #
333
+ # :table => The name of the table to which the constraint should be
334
+ # applied.
335
+ # :referenced_table => The name of the table which the foreign key
336
+ # refers to.
337
+ # :fk => The name of the field to turn into a foreign key.
338
+ # :pk => The primary key of the referenced table.
339
+ # :update => The action that should be taken if the primary key
340
+ # of a referenced row is changed.
341
+ # :delete => The action that should be taken if a referenced
342
+ # row is deleted.
343
+ # :name => The name of the constraint to apply.
344
+
345
+ def constraint_info(rel)
346
+ if rel.join_table
347
+ info = join_table_info(rel.owner_class,rel.target_class)
348
+ constraints = [ { :fk => info[:first_key], :referenced_table => info[:first_table], :table => rel.join_table, :pk => ( rel.owner_class.primary_key.field || rel.owner_class.primary_key.symbol ), :update => 'CASCADE', :delete => 'CASCADE'},
349
+ { :fk => info[:second_key], :referenced_table => info[:second_table], :table => rel.join_table, :pk => ( rel.target_class.primary_key.field || rel.target_class.primary_key.symbol ), :update => 'CASCADE', :delete => 'CASCADE' } ]
350
+ elsif rel.class == Og::HasMany
351
+ constraints = [ { :fk => rel.foreign_key, :table => rel.target_class::OGTABLE, :referenced_table => rel.owner_class::OGTABLE, :pk => ( rel.owner_class.primary_key.field || rel.owner_class.primary_key.symbol ), :update => 'SET NULL', :delete => 'SET NULL' } ]
352
+ else
353
+ constraints = [ { :fk => rel.foreign_key, :table => rel.owner_class::OGTABLE, :referenced_table => rel.target_class::OGTABLE, :pk => ( rel.target_class.primary_key.field || rel.target_class.primary_key.symbol ), :update => 'SET NULL', :delete => 'SET NULL' } ]
354
+ end
355
+
356
+ constraints.each do |constraint|
357
+ constraint[:name] = constraint_name(constraint)
358
+ end
359
+
360
+ # This checks for not-yet-enchanted entities, is there a better way?
361
+ constraints.reject{|info| [info[:table], info[:referenced_table]].include?(:OGTABLE) }
362
+ end
255
363
 
256
- private
257
-
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).
262
-
263
- def create_join_table_foreign_key_constraints(klass,info)
264
364
 
265
- table_list = @conn.list_tables
365
+ # Returns a hash keyed by table (as a string) with each value also
366
+ # being a hash keyed by the constraint name (as a string) and the
367
+ # value being a string that contains the constraint definition.
368
+ #
369
+ # This format matches the actual constrains returned by the
370
+ # all_foreign_keys method added to the PGConn class.
371
+
372
+ def all_needed_constraints
373
+ relations = manager.managed_classes.map{|klass| klass.relations}.flatten.uniq
374
+ need_constraints = Hash.new
375
+ relations.each do |relation|
376
+ infos = constraint_info(relation)
377
+ infos.each do |info|
378
+ # Skip constraints we already know we need
379
+ next if need_constraints[info[:table]] and need_constraints[info[:table]].has_key? info[:name]
380
+ need_constraints[info[:table]] ||= Hash.new
381
+ need_constraints[info[:table]][info[:name]] = constraint_definition(info)
382
+ end
383
+ end
384
+ need_constraints
385
+ end
266
386
 
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)
387
+ # Returns an SQL fragment containing the correct definition for a foreign key constraint.
388
+
389
+ def constraint_definition(info)
390
+ "FOREIGN KEY (#{info[:fk]}) REFERENCES #{info[:referenced_table]}(#{info[:pk]}) ON UPDATE #{info[:update]} ON DELETE #{info[:delete]}"
391
+ end
392
+
393
+ # Works the same as all_needed_constraints but only acts on one class and
394
+ # returns the same hash as part of yet another hash with two keys, tables
395
+ # and constraints. This is done to prevent having to resolve the
396
+ # relations again later just to map tables.
397
+
398
+ def needed_constraints(klass)
399
+ need_constraints = Hash.new
400
+ tables = Array.new
401
+ (klass.relations + klass.resolve_remote_relations).each do |rel|
402
+ constraint_info(rel).each do |info|
403
+ tables.concat [info[:table], info[:referenced_table]]
404
+ need_constraints[info[:table]] ||= Hash.new
405
+ need_constraints[info[:table]][info[:name]] = constraint_definition(info)
406
+ end
407
+ end
408
+ { :tables => tables.uniq, :constraints => need_constraints }
409
+ end
410
+
411
+ # Returns the appropriate constraint prefix for a foreign key constraint.
412
+
413
+ def constraint_prefix
414
+ "#{Og.table_prefix}c"
415
+ end
416
+
417
+ # Returns the appropriate name for a constraint element generated by
418
+ # the constraint_info method.
419
+
420
+ def constraint_name(hash)
421
+ "#{constraint_prefix}_#{hash[:table]}_#{hash[:fk]}"
422
+ end
423
+
424
+ def needed_constraints_sql(klass = nil)
425
+ if klass
426
+ constraints = needed_constraints(klass)
427
+ all_needed = constraints[:constraints]
428
+ all_existing = Hash.new
429
+ constraints[:tables].each do |table|
430
+ all_existing[table] = @conn.table_foreign_keys(table)
431
+ end
432
+ else
433
+ all_existing = @conn.all_foreign_keys
434
+ all_needed = all_needed_constraints
271
435
  end
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}, "
436
+
437
+ drop_constraints = Array.new
438
+ create_constraints = Array.new
439
+
440
+ all_needed.each_pair do |table,constraints|
441
+ constraints.each_pair do |name,definition|
442
+
443
+ # If neither of these are matched, the constraint already exists
444
+ # and has the correct definition.
445
+
446
+ if all_existing[table].nil? or all_existing[table][name].nil?
447
+
448
+ # Does not exist in database
449
+
450
+ create_constraints << "ALTER TABLE #{table} ADD CONSTRAINT #{name} #{definition}"
451
+ elsif all_existing[table][name] != definition
452
+
453
+ # Exists in database and matches the object structure but has the
454
+ # wrong definition (unlikely to happen very often).
455
+
456
+ Logger.debug "PostgreSQL database contains a constraint on table '#{table}' named '#{name}' which is incorrectly defined and will be redefined (OLD: '#{all_existing[table][name]}', NEW: '#{definition}')"
457
+ drop_constraints << "ALTER TABLE #{table} DROP CONSTRAINT #{name}"
458
+ create_constraints << "ALTER TABLE #{table} ADD CONSTRAINT #{name} #{definition}"
459
+ end
278
460
  end
279
- Logger.debug msg[0..-3] + ". (Should be retried later)."
280
- return false
281
461
  end
282
462
 
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
463
+ # You can't do this when managing classes seperately without spidering
464
+ # each other class managed by this stores manager as other classes
465
+ # can want relations within the same tables too. I will add spidering
466
+ # support at some point but this isn't very important since these
467
+ # complicated and convoluted routines will now rarely happen thank
468
+ # to the setup hooking.
469
+
470
+ unless klass
471
+ all_existing.each_pair do |table,constraints|
472
+ constraints.each_key do |name|
473
+ if all_needed[table].nil? or all_needed[table][name].nil?
474
+
475
+ # Exists in database but doesn't match object model at all
476
+ raise Exception if table.to_s.downcase == "table"
477
+ Logger.debug "PostgreSQL database contains a constraint on table '#{table}' named '#{name}' which does not match the object model and will be deleted"
478
+ drop_constraints << "ALTER TABLE #{table} DROP CONSTRAINT #{name}"
479
+ end
480
+ end
289
481
  end
290
482
  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
483
 
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] }
484
+ {
485
+ :drop => drop_constraints,
486
+ :create => create_constraints
487
+ }
488
+
489
+ end
490
+
491
+ # Takes a hash with constraints to drop and create and performs
492
+ # the work.
493
+
494
+ def create_constraints(param = nil)
495
+ subsection_only = !!param
496
+ sql_hash = param ? param : needed_constraints_sql
497
+ Logger.debug "PostgreSQL processing foreign key constraints" unless subsection_only
498
+ started = Time.now
499
+ deleted = 0
500
+ nulled_relations = 0
501
+ deleted_relations = 0
502
+ created = 0
503
+
504
+ sql_hash[:drop].each do |sql|
505
+ begin
506
+ @conn.exec(sql)
507
+ rescue RuntimeError => ex
508
+ raise unless ex.message =~ /does not exist/
509
+ end
510
+ deleted += 1
511
+ end
304
512
 
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
513
+ sql_hash[:create].each do |sql|
514
+ con_retry = true
515
+ begin
516
+ @conn.exec(sql)
517
+ created += 1
518
+ rescue PGError,RuntimeError => ex
519
+ next if ex.message =~ /already exists/ # Racing
520
+ unless ex.message =~ /.*violates foreign key constraint.*/
521
+ Logger.error "PostgreSQL connection returned an error for query #{sql}"
522
+ raise
523
+ end
524
+ if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false
525
+ table, name, fk, referenced_table, pk = sql.match(/^ALTER TABLE (\S+) ADD CONSTRAINT (\S+) FOREIGN KEY \((\S+)\) REFERENCES ([^ (]+)[ (]+([^)]+)/).captures
526
+ raise if [table,fk,pk,referenced_table].include? nil
527
+
528
+ cleaner_sql = "UPDATE #{table} SET #{fk} = NULL WHERE #{fk} NOT IN (SELECT #{pk} FROM #{referenced_table})"
529
+ begin
530
+ @conn.exec(cleaner_sql)
531
+ if cleaner_sql[0..5] == "UPDATE"
532
+ nulled_relations += 1
533
+ else
534
+ deleted_relations += 1
535
+ end
536
+
537
+ rescue PGError,RuntimeError => ex
538
+ if ex.message =~ /.*violates not-null constraint.*/
539
+ cleaner_sql = "DELETE FROM #{table} WHERE #{fk} NOT IN (SELECT #{pk} FROM #{referenced_table})"
540
+ retry
541
+ end
542
+ Logger.error "PostgreSQL connection returned an error for query '#{cleaner_sql}' which was attempting to tidy up ready for the query '#{sql}'"
543
+ raise
544
+ end
545
+
546
+ Logger.error "There were relationships in table #{table} that did not exist so they have been set to NULL (or deleted if this was not possible, i.e. for a join table)."
547
+ if con_retry
548
+ con_retry = false
549
+ retry
550
+ end
551
+ else
552
+ Logger.error "There are relationships in table #{table} that do not exist. Your database is corrupt. Please fix these or enable evolve_schema not in cautious mode and they will be fixed automatically."
553
+ end
554
+ end
555
+ end
556
+ finished = Time.now
557
+ taken = Kernel.sprintf("%.2f", finished - started)
558
+ broken_relations = nulled_relations + deleted_relations
559
+ text = "PostgreSQL finished setting constraints. "
560
+ need_comma = false
561
+ if [0,0,0] == [deleted,created,broken_relations]
562
+ return if subsection_only # Make less chatty for short calls
563
+ text << "No action was taken, "
564
+ else
565
+ text << "#{created} constraints were added, " if created != 0
566
+ text << "#{deleted} constraints were deleted, " if deleted != 0
567
+ if broken_relations != 0
568
+ text.gsub!(/,([^,]+)$/,' and \1')
569
+ text << "#{broken_relations} relations were broken, causing "
570
+ if nullified_relations != 0
571
+ text << "#{nullified_relations} relations to have non-existant foreign keys set to null"
572
+ text << (deleted_relations == 0 ? ", " : " and ")
310
573
  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})."
574
+ text << "#{nullified_relations} relations to have rows with non-existant foreign keys deleted, " if deleted_relations != 0
314
575
  end
315
576
  end
577
+ text = text[0..-3].gsub(/,([^,]+)$/,' and \1')
578
+ text << " in #{taken} seconds."
579
+ Logger.debug text
580
+ end
581
+
582
+ # Called by Og.manager (in turn called by Og.setup) when Og.setup
583
+ # has finished, allowing better processing of foreign key
584
+ # constraints and possibly other enhancements.
585
+
586
+ def post_setup
587
+ create_constraints
316
588
  end
589
+
590
+ private
317
591
 
318
592
  def create_table(klass)
319
593
  fields = fields_for_class(klass)
@@ -341,9 +615,17 @@ private
341
615
  sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
342
616
  end
343
617
  end
344
-
345
- @conn.exec(sql).clear
346
- Logger.info "Created table '#{klass::OGTABLE}'."
618
+ begin
619
+ res = @conn.exec(sql)
620
+ res.clear
621
+ Logger.info "Created table '#{klass::OGTABLE}'."
622
+ rescue RuntimeError => ex
623
+ catch :ok do # Racing
624
+ throw :ok if ex.message =~ /duplicate key violates unique constraint "pg_class_relname_nsp_index"/
625
+ throw :ok if ex.message =~ /already exists/
626
+ raise
627
+ end
628
+ end
347
629
  else
348
630
  Logger.debug "Table #{klass::OGTABLE} already exists"
349
631
  #rp: basic field interrogation
@@ -359,7 +641,11 @@ private
359
641
  if @options[:evolve_schema] == true
360
642
  Logger.debug "Adding field '#{needed_field}' to '#{klass::OGTABLE}'"
361
643
  sql = "ALTER TABLE #{klass::OGTABLE} ADD COLUMN #{needed_field}"
362
- @conn.exec(sql)
644
+ begin
645
+ @conn.exec(sql)
646
+ rescue RuntimeError => ex
647
+ raise unless ex.message =~ /already exists/
648
+ end
363
649
  else
364
650
  Logger.info "WARNING: Table '#{klass::OGTABLE}' is missing field '#{needed_field}' and :evolve_schema is not set to true!"
365
651
  end
@@ -371,8 +657,12 @@ private
371
657
  next if needed_fields.include?(obsolete_field)
372
658
  if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false
373
659
  sql = "ALTER TABLE #{klass::OGTABLE} DROP COLUMN #{obsolete_field}"
374
- Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass::OGTABLE}'"
375
- @conn.exec(sql)
660
+ begin
661
+ @conn.exec(sql)
662
+ rescue RuntimeError => ex
663
+ raise unless ex.message =~ /does not exist/
664
+ Logger.debug "Removed obsolete field '#{obsolete_field}' from '#{klass::OGTABLE}'"
665
+ end
376
666
  else
377
667
  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
668
  end
@@ -381,40 +671,77 @@ private
381
671
 
382
672
  # Create join tables if needed. Join tables are used in
383
673
  # 'many_to_many' relations.
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
674
+
675
+ # For some reason this is missing a self join case
676
+ # and therefore can't be used.
677
+ # if join_tables = klass.ann.self[:join_tables]
678
+ # for info in join_tables
679
+ # unless @conn.table_exists? info[:table]
680
+ # join_tables = Array.new
681
+ join_tables = klass.relations.reject{|rel| !rel.join_table}.map{|rel| join_table_info(rel.owner_class, rel.target_class)}
682
+ for info in join_tables
683
+ unless @conn.table_exists? info[:table]
684
+ create_join_table_sql(info).each do |sql|
685
+ begin
686
+ res = @conn.exec(sql)
687
+ res.clear
688
+ rescue RuntimeError => ex
689
+ raise unless ex.message =~ /duplicate key violates unique constraint "pg_class_relname_nsp_index"/
690
+ # Racing
392
691
  end
393
- Logger.debug "Created jointable '#{info[:table]}'."
394
- else
395
- Logger.debug "Join table '#{info[:table]}' already exists."
396
692
  end
397
- create_join_table_foreign_key_constraints(player,info)
693
+ Logger.debug "Created jointable '#{info[:table]}'."
694
+ else
695
+ Logger.debug "Join table '#{info[:table]}' already exists."
398
696
  end
399
697
  end
698
+
699
+ # If we are being called by Og.setup, we can use a much cleaner method
700
+ # for constructing foreign key constraints.
701
+ return if @options[:called_by_og_setup]
702
+
703
+ # Strip out old constraints... this shouldn't always be necessary but
704
+ # must be here for now while glycerin is still bleeding-edge to fix
705
+ # changes and a nasty error that made it into the glycerin developers
706
+ # darcs repo (but NOT into any released version of Nitro)
707
+
708
+ unless @options[:leave_constraints] == true or @stripped_constraints
709
+ Logger.debug "Stripping PostgreSQL foreign key constraints"
710
+ all_foreign_keys.map{|k| k[1].map{|v| [k[0],v[0]] }[0]}.each do |table,constraint|
711
+ prefix = constraint_prefix
712
+ next unless constraint[0-prefix.size..-1] == constraint_prefix
713
+ begin
714
+ m.store.conn.exec "ALTER TABLE #{table} DROP CONSTRAINT #{constraint}"
715
+ rescue Exception
716
+ end
717
+ end
400
718
  end
719
+
720
+ # Create sql constraints
721
+ create_constraints(needed_constraints_sql(klass))
401
722
 
402
723
  end
403
724
 
404
725
  def drop_table(klass)
405
726
  # foreign key constraints will remove the need to do manual cleanup on
406
- # postgresql join tables.
727
+ # related rows.
407
728
  exec "DROP TABLE #{klass.table} CASCADE"
408
729
  end
409
730
 
410
731
  def create_field_map(klass)
411
- res = @conn.exec "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
732
+ begin
733
+ res = @conn.exec "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
734
+ rescue RuntimeError => ex
735
+ raise unless ex.message =~ /does not exist/ or ex.message =~ /deleted while still in use/
736
+ # Racing
737
+ create_table(klass)
738
+ retry
739
+ end
412
740
  map = {}
413
741
 
414
742
  for field in res.fields
415
743
  map[field.intern] = res.fieldnum(field)
416
744
  end
417
-
418
745
  return map
419
746
  ensure
420
747
  res.clear if res
@@ -496,4 +823,4 @@ end
496
823
  # * George Moschovitis <gm@navel.gr>
497
824
  # * Michael Neumann <mneumann@ntecs.de>
498
825
  # * Ysabel <deb@ysabel.org>
499
- # * Rob Pitt <rob@motionpath.com>
826
+ # * Rob Pitt <rob@motionpath.com>