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.
- data/ProjectInfo +2 -2
- data/README +1 -1
- data/doc/RELEASES +78 -0
- data/lib/glue/revisable.rb +123 -1
- data/lib/glue/searchable.rb +28 -0
- data/lib/glue/taggable.rb +18 -3
- data/lib/glue/timestamped.rb +5 -5
- data/lib/og.rb +28 -4
- data/lib/og/collection.rb +15 -0
- data/lib/og/entity.rb +64 -44
- data/lib/og/evolution.rb +1 -1
- data/lib/og/manager.rb +16 -0
- data/lib/og/relation.rb +5 -1
- data/lib/og/relation/refers_to.rb +6 -0
- data/lib/og/store.rb +13 -0
- data/lib/og/store/kirby.rb +6 -5
- data/lib/og/store/mysql.rb +34 -5
- data/lib/og/store/psql.rb +409 -82
- data/lib/og/store/sql.rb +27 -11
- data/lib/og/store/sqlite.rb +3 -3
- data/lib/og/validation.rb +53 -35
- data/test/glue/tc_revisable.rb +62 -0
- data/test/og/CONFIG.rb +2 -2
- data/test/og/store/tc_kirby.rb +31 -0
- data/test/og/tc_finder.rb +20 -0
- data/test/og/tc_validation.rb +53 -0
- metadata +9 -6
- data/lib/glue/taggable_old.rb +0 -228
- data/lib/og/vendor/kbserver.rb +0 -20
- data/lib/og/vendor/kirbybase.rb +0 -2941
data/lib/og/evolution.rb
CHANGED
data/lib/og/manager.rb
CHANGED
@@ -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
|
data/lib/og/relation.rb
CHANGED
@@ -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]
|
data/lib/og/store.rb
CHANGED
@@ -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
|
|
data/lib/og/store/kirby.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
begin
|
2
|
-
require '
|
2
|
+
require 'kirbybase'
|
3
3
|
rescue Object => ex
|
4
|
-
Logger.error
|
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, :
|
62
|
-
klass.send :alias_method, :
|
63
|
-
klass.send :alias_method, :
|
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} }"
|
data/lib/og/store/mysql.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
280
|
-
|
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.
|
data/lib/og/store/psql.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
52
|
-
#
|
53
|
-
#
|
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
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
#
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
|
-
|
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
|
-
|
346
|
-
|
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
|
-
|
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
|
-
|
375
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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
|
-
|
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
|
-
#
|
727
|
+
# related rows.
|
407
728
|
exec "DROP TABLE #{klass.table} CASCADE"
|
408
729
|
end
|
409
730
|
|
410
731
|
def create_field_map(klass)
|
411
|
-
|
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>
|