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/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,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2007-2011 Ease Software, Inc. and Perry Smith
4
+ # All Rights Reserved
5
+ #
6
+ # Install hook code here
@@ -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