activerecord_constraints 0.1.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/GNU-LICENSE +674 -0
- data/README +193 -0
- data/Rakefile +42 -0
- data/init.rb +45 -0
- data/install.rb +6 -0
- data/lib/activerecord_constraint_handlers.rb +307 -0
- data/lib/activerecord_constraints.rb +433 -0
- data/lib/tasks/activerecord_constraints_tasks.rake +4 -0
- data/test/database.yml +13 -0
- data/test/migration/double_connection_test.rb +122 -0
- data/test/migration/unique_constraints_multi_test.rb +95 -0
- data/test/migration/unique_constraints_null_test.rb +93 -0
- data/test/migration/unique_constraints_test.rb +87 -0
- data/test/test_helper.rb +76 -0
- data/uninstall.rb +6 -0
- metadata +70 -0
data/README
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
= ActiveRecord Constraints
|
2
|
+
|
3
|
+
This plugin currently implements constraints for PostgreSQL only. It
|
4
|
+
should provide a structure for a more abstract implementation.
|
5
|
+
|
6
|
+
Currently it implements foreign key constraints, unique
|
7
|
+
constraints and check constraints. null and not null constraints
|
8
|
+
would be easy to add but they may collide with preexisting Active
|
9
|
+
Record code.
|
10
|
+
|
11
|
+
=== Examples
|
12
|
+
|
13
|
+
First the easy examples:
|
14
|
+
|
15
|
+
Unique Constraint:
|
16
|
+
|
17
|
+
class CreateFoos < ActiveRecord::Migration
|
18
|
+
def self.up
|
19
|
+
create_table :foos do |t|
|
20
|
+
# name will have a "unique" constraint
|
21
|
+
t.string :name, :null => false, :unique => true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Trivial foreign key constraint:
|
27
|
+
|
28
|
+
class CreateFoos < ActiveRecord::Migration
|
29
|
+
def self.up
|
30
|
+
create_table :foos do |t|
|
31
|
+
# bar_id will now be a foreign key constraint column id in table bar
|
32
|
+
t.integer :bar_id, :null => false, :reference => true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
This is actually a short hand for:
|
38
|
+
|
39
|
+
class CreateFoos < ActiveRecord::Migration
|
40
|
+
def self.up
|
41
|
+
create_table :foos do |t|
|
42
|
+
# bar_id will now be a foreign key constraint column id in table bar
|
43
|
+
t.integer :bar_id, :null => false, :reference => true,
|
44
|
+
:table_name => :bars, :foreign_key => :id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
If the constraint can not be done this easily, there are also unique,
|
50
|
+
reference, and check methods added to
|
51
|
+
ActiveRecord::ConnectionAdapters::TableDefinition. So, for example:
|
52
|
+
|
53
|
+
class CreateFoos < ActiveRecord::Migration
|
54
|
+
def self.up
|
55
|
+
create_table :foos do |t|
|
56
|
+
t.string :name1, :null => false
|
57
|
+
t.string :name2, :null => false
|
58
|
+
t.string :name3, :null => false
|
59
|
+
t.unique [ :name1, :name2, :name3 ]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Or perhaps:
|
65
|
+
|
66
|
+
class CreateFoos < ActiveRecord::Migration
|
67
|
+
def self.up
|
68
|
+
create_table :foos do |t|
|
69
|
+
t.integer :field1, :null => false
|
70
|
+
t.integer :field2, :null => false
|
71
|
+
t.integer :field3, :null => false
|
72
|
+
t.reference [ :field1, :field2, :field3 ],
|
73
|
+
:table_name => :bars,
|
74
|
+
:foreign_key => [ :bar_field1, :bar_field2, :bar_field3 ]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Thats the front half. The back half is catching the exceptions during
|
80
|
+
a save. For example, if we have:
|
81
|
+
|
82
|
+
class CreateFoos < ActiveRecord::Migration
|
83
|
+
def self.up
|
84
|
+
create_table :foos do |t|
|
85
|
+
# name will have a "unique" constraint
|
86
|
+
t.string :name, :null => false, :unique => true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
And then if we do:
|
92
|
+
|
93
|
+
foo = Foo.new()
|
94
|
+
foo.save
|
95
|
+
|
96
|
+
The save will throw an exception but the semantics of foo.save is to
|
97
|
+
return false if the constraints (validations) fail. To keep this API,
|
98
|
+
the exception is caught and parsed trying to do what the standard
|
99
|
+
Rails constraints do. In the above example, foo.errors.on(:name) will
|
100
|
+
be set to "can't be blank".
|
101
|
+
|
102
|
+
Contraints may also be named. This allows the rescue to call a
|
103
|
+
specific method by the same name as the constraint.
|
104
|
+
|
105
|
+
=== Help with testing and fixtures
|
106
|
+
|
107
|
+
To work with the new fixtures, you must patch Rails:
|
108
|
+
|
109
|
+
module ActiveRecord
|
110
|
+
module ConnectionAdapters
|
111
|
+
class PostgreSQLAdapter
|
112
|
+
def disable_referential_integrity(&block)
|
113
|
+
transaction {
|
114
|
+
begin
|
115
|
+
execute "SET CONSTRAINTS ALL DEFERRED"
|
116
|
+
yield
|
117
|
+
ensure
|
118
|
+
execute "SET CONSTRAINTS ALL IMMEDIATE"
|
119
|
+
end
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
Setting the schema_format to :sql is also a good idea. In
|
127
|
+
environment.rb add:
|
128
|
+
|
129
|
+
config.active_record.schema_format = :sql
|
130
|
+
|
131
|
+
And then you must make all the foreign key constraints deferrable.
|
132
|
+
So, the above example becomes:
|
133
|
+
|
134
|
+
class CreateFoos < ActiveRecord::Migration
|
135
|
+
def self.up
|
136
|
+
create_table :foos do |t|
|
137
|
+
# bar_id will now be a foreign key constraint column id in table bar
|
138
|
+
t.integer :bar_id, :null => false, :reference => true,
|
139
|
+
:deferrable => true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
I like to have my foreign keys cascade on delete so now we have:
|
145
|
+
|
146
|
+
class CreateFoos < ActiveRecord::Migration
|
147
|
+
def self.up
|
148
|
+
create_table :foos do |t|
|
149
|
+
# bar_id will now be a foreign key constraint on column id in table bar
|
150
|
+
t.integer :bar_id, :null => false, :reference => true,
|
151
|
+
:deferrable => true, :delete => :cascade
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
But really I hate typing all that so, it now becomes:
|
157
|
+
|
158
|
+
class CreateFoos < ActiveRecord::Migration
|
159
|
+
def self.up
|
160
|
+
create_table :foos do |t|
|
161
|
+
# bar_id will now be a foreign key constraint column id in table bar
|
162
|
+
t.fk :bar_id
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
_fk_ is currently a silly stupid thing that needs to be fleshed out
|
168
|
+
more but the intent to make a "foreign key" something that Rails will
|
169
|
+
grok.
|
170
|
+
|
171
|
+
=== Future Directions
|
172
|
+
|
173
|
+
All the work code is in
|
174
|
+
ActiveRecord::ConnectionAdapters::Constraints. For other data base
|
175
|
+
engines, this module
|
176
|
+
|
177
|
+
Copyright (c) 2009 Perry Smith
|
178
|
+
|
179
|
+
This file is part of activerecord_constraints.
|
180
|
+
|
181
|
+
activerecord_constraints is free software: you can redistribute it
|
182
|
+
and/or modify it under the terms of the GNU General Public License as
|
183
|
+
published by the Free Software Foundation, either version 3 of the
|
184
|
+
License, or (at your option) any later version.
|
185
|
+
|
186
|
+
activerecord_constraints is distributed in the hope that it will be
|
187
|
+
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
188
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
189
|
+
General Public License for more details.
|
190
|
+
|
191
|
+
You should have received a copy of the GNU General Public License
|
192
|
+
along with activerecord_constraints. If not, see
|
193
|
+
<http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
# Copyright (c) 2009 Perry Smith
|
3
|
+
|
4
|
+
# This file is part of activerecord_constraints.
|
5
|
+
|
6
|
+
# activerecord_constraints is free software: you can redistribute it
|
7
|
+
# and/or modify it under the terms of the GNU General Public License as
|
8
|
+
# published by the Free Software Foundation, either version 3 of the
|
9
|
+
# License, or (at your option) any later version.
|
10
|
+
|
11
|
+
# activerecord_constraints is distributed in the hope that it will be
|
12
|
+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14
|
+
# General Public License for more details.
|
15
|
+
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with activerecord_constraints. If not, see
|
18
|
+
# <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require 'rake'
|
21
|
+
require 'rake/testtask'
|
22
|
+
require 'rake/rdoctask'
|
23
|
+
|
24
|
+
desc 'Default: run unit tests.'
|
25
|
+
task :default => :test
|
26
|
+
|
27
|
+
desc 'Test the activerecord_constraints plugin.'
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'lib'
|
30
|
+
t.libs << 'test'
|
31
|
+
t.pattern = 'test/**/*_test.rb'
|
32
|
+
t.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Generate documentation for the activerecord_constraints plugin.'
|
36
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
37
|
+
rdoc.rdoc_dir = 'rdoc'
|
38
|
+
rdoc.title = 'PostgresConstraints'
|
39
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
40
|
+
rdoc.rdoc_files.include('README')
|
41
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
42
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2007-2011 Ease Software, Inc. and Perry Smith
|
4
|
+
# All Rights Reserved
|
5
|
+
#
|
6
|
+
|
7
|
+
# Copyright (c) 2009 Perry Smith
|
8
|
+
|
9
|
+
# This file is part of activerecord_constraints.
|
10
|
+
|
11
|
+
# activerecord_constraints is free software: you can redistribute it
|
12
|
+
# and/or modify it under the terms of the GNU General Public License as
|
13
|
+
# published by the Free Software Foundation, either version 3 of the
|
14
|
+
# License, or (at your option) any later version.
|
15
|
+
|
16
|
+
# activerecord_constraints is distributed in the hope that it will be
|
17
|
+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
19
|
+
# General Public License for more details.
|
20
|
+
|
21
|
+
# You should have received a copy of the GNU General Public License
|
22
|
+
# along with activerecord_constraints. If not, see
|
23
|
+
# <http://www.gnu.org/licenses/>.
|
24
|
+
|
25
|
+
require 'activerecord_constraints'
|
26
|
+
require 'activerecord_constraint_handlers'
|
27
|
+
|
28
|
+
ActiveRecord::Base.schema_format = :sql
|
29
|
+
|
30
|
+
module ::ActiveRecord
|
31
|
+
module ConnectionAdapters
|
32
|
+
class PostgreSQLAdapter
|
33
|
+
def disable_referential_integrity(&block)
|
34
|
+
transaction {
|
35
|
+
begin
|
36
|
+
execute "SET CONSTRAINTS ALL DEFERRED"
|
37
|
+
yield
|
38
|
+
ensure
|
39
|
+
execute "SET CONSTRAINTS ALL IMMEDIATE"
|
40
|
+
end
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/install.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2007-2011 Ease Software, Inc. and Perry Smith
|
4
|
+
# All Rights Reserved
|
5
|
+
#
|
6
|
+
|
7
|
+
# Copyright (c) 2009 Perry Smith
|
8
|
+
|
9
|
+
# This file is part of activerecord_constraints.
|
10
|
+
|
11
|
+
# activerecord_constraints is free software: you can redistribute it
|
12
|
+
# and/or modify it under the terms of the GNU General Public License as
|
13
|
+
# published by the Free Software Foundation, either version 3 of the
|
14
|
+
# License, or (at your option) any later version.
|
15
|
+
|
16
|
+
# activerecord_constraints is distributed in the hope that it will be
|
17
|
+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
19
|
+
# General Public License for more details.
|
20
|
+
|
21
|
+
# You should have received a copy of the GNU General Public License
|
22
|
+
# along with activerecord_constraints. If not, see
|
23
|
+
# <http://www.gnu.org/licenses/>.
|
24
|
+
|
25
|
+
module ActiveRecord
|
26
|
+
module ConnectionAdapters
|
27
|
+
# Constraint Handlers is a module to catch and handle the
|
28
|
+
# exceptions produced by failed constraints. The current
|
29
|
+
# implementation ties only to the create_or_update path which is
|
30
|
+
# after validations are done. This allows the database to have
|
31
|
+
# constraints but preserve the API and semantics of Model.save and
|
32
|
+
# Model.save!
|
33
|
+
#
|
34
|
+
# The three hooks made available is a hook when the connection is
|
35
|
+
# first created. At that point, the database adapter specific
|
36
|
+
# module is loaded into the base. It does not work to rummage
|
37
|
+
# around in the database during this hook. This hook is done via
|
38
|
+
# ConstraintConnectionHook.
|
39
|
+
#
|
40
|
+
# The second hook is done by the adapter specific constrait
|
41
|
+
# handler. The call to create_or_update is captured. If an
|
42
|
+
# exception is thrown, then the adapter specific
|
43
|
+
# handle_create_or_update_exception is called.
|
44
|
+
#
|
45
|
+
# The third hook is also in the create_or_update. Before the call
|
46
|
+
# to create_or_update_witout_constraints is done, the class
|
47
|
+
# specific pre_fetch method is called. Again, this method is
|
48
|
+
# included into the class of the model at the time the connection
|
49
|
+
# is created.
|
50
|
+
#
|
51
|
+
module ConstraintHandlers
|
52
|
+
|
53
|
+
# A PostgreSQL specific implementation.
|
54
|
+
module Postgresql
|
55
|
+
|
56
|
+
NOT_NULL_REGEXP = Regexp.new("PGError: +ERROR: +null value in column \"([^\"]*)\" violates not-null constraint")
|
57
|
+
UNIQUE_REGEXP = Regexp.new("PGError: +ERROR: +duplicate key .*violates unique constraint \"([^\"]+)\"")
|
58
|
+
FOREIGN_REGEXP = Regexp.new("PGError: +ERROR: +insert or update on table \"([^\"]+)\" violates " +
|
59
|
+
"foreign key constraint \"([^\"]+)\"")
|
60
|
+
#
|
61
|
+
# We need class methods and class instance variables to hold
|
62
|
+
# the data. We want them in the class so that the work is
|
63
|
+
# done only once for the life of the application. In the
|
64
|
+
# PostgreSQL case, we leverage off of ActiveRecord::Base by
|
65
|
+
# creating three nested models so they are hidden
|
66
|
+
# syntactically that use the model as their base class so that
|
67
|
+
# they use the same connection as the model itself. This
|
68
|
+
# allows other models to use other connections and the data is
|
69
|
+
# kept separate.
|
70
|
+
module ClassMethods
|
71
|
+
@pg_class = nil
|
72
|
+
@pg_constraints = nil
|
73
|
+
@pg_constraint_hash = nil
|
74
|
+
@pg_attributes = nil
|
75
|
+
@pg_attribute_hash = nil
|
76
|
+
|
77
|
+
# Create the constant for the PgClass nested model.
|
78
|
+
def pg_class_constant
|
79
|
+
"#{self}::PgClass".constantize
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create the constant for the PgConstraint nested model.
|
83
|
+
def pg_constraint_constant
|
84
|
+
"#{self}::PgConstraint".constantize
|
85
|
+
end
|
86
|
+
|
87
|
+
# Create the constant for the PgAttribute nested model.
|
88
|
+
def pg_attribute_constant
|
89
|
+
"#{self}::PgAttribute".constantize
|
90
|
+
end
|
91
|
+
|
92
|
+
# Turns out, we don't really use this...
|
93
|
+
def pg_class
|
94
|
+
ActiveRecord::Base.logger.debug("pg_class")
|
95
|
+
@pg_class ||= pg_class_constant.find_by_relname(table_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Find the constraints for this model / table
|
99
|
+
def pg_constraints
|
100
|
+
ActiveRecord::Base.logger.debug("pg_constraints")
|
101
|
+
if @pg_constraints.nil?
|
102
|
+
@pg_constraints = pg_constraint_constant.find(:all,
|
103
|
+
:joins => :conrel,
|
104
|
+
:conditions => { :pg_class => { :relname => table_name }})
|
105
|
+
@pg_constraint_hash = Hash.new
|
106
|
+
@pg_constraints.each { |c|
|
107
|
+
ActiveRecord::Base.logger.debug("Adding '#{c.conname}' to constraint_hash")
|
108
|
+
@pg_constraint_hash[c.conname] = c
|
109
|
+
}
|
110
|
+
end
|
111
|
+
@pg_constraints
|
112
|
+
end
|
113
|
+
|
114
|
+
# Accessor for the constraint hash
|
115
|
+
def pg_constraint_hash
|
116
|
+
@pg_constraint_hash
|
117
|
+
end
|
118
|
+
|
119
|
+
# Find the attributes for this model
|
120
|
+
def pg_attributes
|
121
|
+
ActiveRecord::Base.logger.debug("pg_attributes")
|
122
|
+
if @pg_attributes.nil?
|
123
|
+
@pg_attributes = pg_attribute_constant.find(:all,
|
124
|
+
:joins => :attrel,
|
125
|
+
:conditions => { :pg_class => { :relname => table_name }})
|
126
|
+
@pg_attribute_hash = Hash.new
|
127
|
+
@pg_attributes.each { |a| @pg_attribute_hash[a.attnum] = a }
|
128
|
+
end
|
129
|
+
@pg_attributes
|
130
|
+
end
|
131
|
+
|
132
|
+
# Accessor for the attribute hash
|
133
|
+
def pg_attribute_hash
|
134
|
+
@pg_attribute_hash
|
135
|
+
end
|
136
|
+
|
137
|
+
# At the time of the first call, we create the models needed
|
138
|
+
# for the code above as a nested subclass of the model using
|
139
|
+
# the model as the base.
|
140
|
+
def create_subclasses
|
141
|
+
ActiveRecord::Base.logger.debug("create_subclasses")
|
142
|
+
self.class_eval <<-EOF
|
143
|
+
class PgClass < #{self}
|
144
|
+
set_table_name "pg_class"
|
145
|
+
set_primary_key "oid"
|
146
|
+
self.default_scoping = []
|
147
|
+
end
|
148
|
+
|
149
|
+
class PgAttribute < #{self}
|
150
|
+
set_table_name "pg_attribute"
|
151
|
+
set_primary_key "oid"
|
152
|
+
belongs_to :attrel, :class_name => "PgClass", :foreign_key => :attrelid
|
153
|
+
self.default_scoping = []
|
154
|
+
end
|
155
|
+
|
156
|
+
class PgConstraint < #{self}
|
157
|
+
set_table_name "pg_constraint"
|
158
|
+
set_primary_key "oid"
|
159
|
+
belongs_to :conrel, :class_name => "PgClass", :foreign_key => :conrelid
|
160
|
+
self.default_scoping = []
|
161
|
+
end
|
162
|
+
EOF
|
163
|
+
end
|
164
|
+
|
165
|
+
# We can not rummage around in the database after an error has
|
166
|
+
# occurred or we will get back more errors that an error has
|
167
|
+
# already occurred and further queries will be ignored. So, we
|
168
|
+
# pre-fetch the system tables that we need and save them in our
|
169
|
+
# pockets.
|
170
|
+
def pre_fetch
|
171
|
+
ActiveRecord::Base.logger.debug("pre_fetch #{self} #{table_name} #{@pg_class.nil?}")
|
172
|
+
if @pg_class.nil?
|
173
|
+
create_subclasses
|
174
|
+
pg_class
|
175
|
+
pg_constraints
|
176
|
+
pg_attributes
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Converts the constraint name into a list of column names.
|
181
|
+
def constraint_to_columns(constraint)
|
182
|
+
ActiveRecord::Base.logger.debug("constraint_to_columns: '#{constraint}' (#{constraint.class})")
|
183
|
+
|
184
|
+
# Should never hit this now... added during debugging.
|
185
|
+
unless pg_constraint_hash.has_key?(constraint)
|
186
|
+
ActiveRecord::Base.logger.debug("constraint_to_columns: constraint not found")
|
187
|
+
return
|
188
|
+
end
|
189
|
+
# pg_constraint_hash is a hash from the contraint name to
|
190
|
+
# the constraint. The conkey is a string of the form:
|
191
|
+
# +{2,3,4}+ (with the curly braces). The numbers are
|
192
|
+
# column indexes which we pull out from pg_attribute and
|
193
|
+
# convert to a name. Note that the PostgreSQL tables are
|
194
|
+
# singular in name: pg_constraint and pg_attribute
|
195
|
+
k = pg_constraint_hash[constraint].conkey
|
196
|
+
k[1 ... (k.length - 1)].
|
197
|
+
split(',').
|
198
|
+
map{ |s| pg_attribute_hash[s.to_i].attname }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# When the include of
|
203
|
+
# ActiveRecord::ConnectionAdapters::ConstraintConnectionHook
|
204
|
+
# happens this hook is called with ActiveRecord::Base as the
|
205
|
+
# base. We extend the base with the class methods so they are
|
206
|
+
# class methods and the instance variables are then class
|
207
|
+
# instance variables.
|
208
|
+
def self.included(base)
|
209
|
+
base.extend(ClassMethods)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Called with exception when create_or_update throws an exception
|
213
|
+
def handle_create_or_update_exception(e)
|
214
|
+
raise e until e.is_a? ActiveRecord::StatementInvalid
|
215
|
+
logger.debug("trace_report create_or_update error is '#{e.message}'")
|
216
|
+
if md = NOT_NULL_REGEXP.match(e.message)
|
217
|
+
errors.add(md[1].to_sym, "can't be blank")
|
218
|
+
elsif md = UNIQUE_REGEXP.match(e.message)
|
219
|
+
constraint = md[1]
|
220
|
+
ffoo(constraint, "has already been taken")
|
221
|
+
elsif md = FOREIGN_REGEXP.match(e.message)
|
222
|
+
table = md[1]
|
223
|
+
constraint = md[2]
|
224
|
+
ffoo(constraint, "is invalid")
|
225
|
+
else
|
226
|
+
logger.debug("Nothing matched")
|
227
|
+
end
|
228
|
+
false
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
# Could never figure out what to call this. If the model
|
234
|
+
# responds to the constraint name (i.e. there is a method by
|
235
|
+
# the same name in the model), then it is call (currently with
|
236
|
+
# no arguments). If that is not done then the message is
|
237
|
+
# added to the errors array for the column.
|
238
|
+
def ffoo(constraint, message)
|
239
|
+
ActiveRecord::Base.logger.debug("constraint: '#{constraint.inspect}', message: #{message}")
|
240
|
+
c = constraint.to_sym
|
241
|
+
# define a method in the model with the same name as the
|
242
|
+
# constraint and it will be called so it can do whatever it
|
243
|
+
# wants to.
|
244
|
+
if self.respond_to? c
|
245
|
+
self.send c
|
246
|
+
else
|
247
|
+
columns = self.class.constraint_to_columns(constraint)
|
248
|
+
ActiveRecord::Base.logger.debug("columns are: #{columns.inspect}")
|
249
|
+
columns.each { |name| errors.add(name, message) }
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# This module is included in ActiveRecord::Base. The example
|
256
|
+
# provided is for PostgreSQL. During the connect, the adapter
|
257
|
+
# specific connection routine is called. The name of that routine
|
258
|
+
# is the the adapter type with +_connection+ appened.
|
259
|
+
# e.g. +postgresql_connection+
|
260
|
+
module ConstraintConnectionHook
|
261
|
+
def self.included(base)
|
262
|
+
base.class_eval {
|
263
|
+
class << self
|
264
|
+
# An example for the postgresql adapter. Other adapters
|
265
|
+
# need only add to this list with the proper name.
|
266
|
+
def postgresql_connection_with_constraints(config)
|
267
|
+
ActiveRecord::Base.logger.debug("IN: ConstraintConnectionHook::postgresql_connection_with_constraints #{self}")
|
268
|
+
# Pass the call up the chain
|
269
|
+
v = postgresql_connection_without_constraints(config)
|
270
|
+
|
271
|
+
# After we return, add in Postgres' constraint handlers
|
272
|
+
# into "self". Usually this will be ActiveRecord::Base
|
273
|
+
# but I think if Model.establish_connection is called,
|
274
|
+
# then self will be Model.
|
275
|
+
include ConstraintHandlers::Postgresql
|
276
|
+
|
277
|
+
# Return the original result.
|
278
|
+
v
|
279
|
+
end
|
280
|
+
alias_method_chain :postgresql_connection, :constraints
|
281
|
+
end
|
282
|
+
}
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class Base
|
288
|
+
# Postgres needs a hook so it can load some tables when the
|
289
|
+
# connection is first opened. Other DB's may need a similar hook
|
290
|
+
include ActiveRecord::ConnectionAdapters::ConstraintConnectionHook
|
291
|
+
|
292
|
+
# Insert into the create_or_update call chain
|
293
|
+
def create_or_update_with_constraints
|
294
|
+
begin
|
295
|
+
self.class.pre_fetch if self.class.respond_to? :pre_fetch
|
296
|
+
create_or_update_without_constraints
|
297
|
+
rescue => e
|
298
|
+
if respond_to?(:handle_create_or_update_exception)
|
299
|
+
handle_create_or_update_exception(e)
|
300
|
+
else
|
301
|
+
raise e
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
alias_method_chain :create_or_update, :constraints
|
306
|
+
end
|
307
|
+
end
|