activerecord-postgresql-extensions 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/MIT-LICENSE +23 -0
  2. data/README.rdoc +32 -0
  3. data/Rakefile +42 -0
  4. data/VERSION +1 -0
  5. data/lib/activerecord-postgresql-extensions.rb +30 -0
  6. data/lib/postgresql_extensions/foreign_key_associations.rb +367 -0
  7. data/lib/postgresql_extensions/postgresql_adapter_extensions.rb +646 -0
  8. data/lib/postgresql_extensions/postgresql_constraints.rb +579 -0
  9. data/lib/postgresql_extensions/postgresql_functions.rb +345 -0
  10. data/lib/postgresql_extensions/postgresql_geometry.rb +212 -0
  11. data/lib/postgresql_extensions/postgresql_indexes.rb +219 -0
  12. data/lib/postgresql_extensions/postgresql_languages.rb +80 -0
  13. data/lib/postgresql_extensions/postgresql_permissions.rb +322 -0
  14. data/lib/postgresql_extensions/postgresql_rules.rb +112 -0
  15. data/lib/postgresql_extensions/postgresql_schemas.rb +49 -0
  16. data/lib/postgresql_extensions/postgresql_sequences.rb +222 -0
  17. data/lib/postgresql_extensions/postgresql_tables.rb +308 -0
  18. data/lib/postgresql_extensions/postgresql_triggers.rb +131 -0
  19. data/lib/postgresql_extensions/postgresql_types.rb +17 -0
  20. data/lib/postgresql_extensions/postgresql_views.rb +103 -0
  21. data/postgresql-extensions.gemspec +50 -0
  22. data/test/adapter_test.rb +45 -0
  23. data/test/constraints_test.rb +98 -0
  24. data/test/functions_test.rb +112 -0
  25. data/test/geometry_test.rb +43 -0
  26. data/test/index_test.rb +68 -0
  27. data/test/languages_test.rb +48 -0
  28. data/test/permissions_test.rb +163 -0
  29. data/test/rules_test.rb +32 -0
  30. data/test/schemas_test.rb +43 -0
  31. data/test/sequences_test.rb +90 -0
  32. data/test/tables_test.rb +49 -0
  33. data/test/test_helper.rb +64 -0
  34. metadata +97 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2011 2167961 Ontario Inc., Zoocasa <code@zoocasa.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.rdoc ADDED
@@ -0,0 +1,32 @@
1
+
2
+ = PostgreSQL ActiveRecord Extensions
3
+
4
+ What we've got here is a collection of extensions to the ActiveRecord
5
+ PostgreSQL adapter including:
6
+
7
+ * support for constraints in migrations including CHECK constraints, foreign
8
+ keys, unique constraints and so forth. Constraints can be added per column
9
+ and per table.
10
+
11
+ * some methods for manipulating all sorts of stuff like procedural languages,
12
+ functions, sequences, views, schemas and triggers.
13
+
14
+ * better support for creating indexes with expressions so you can use functions
15
+ and GIST and GIN indexes.
16
+
17
+ * support for manipulating role privileges.
18
+
19
+ * support for some PostgreSQL-specific statements like INHERITS and LIKE when
20
+ creating tables.
21
+
22
+ * support for automatically detecting foreign key relationships and building
23
+ ActiveRecord associations accordingly. This associations can be overriden
24
+ as necessary and are sane enough that they won't try to overwrite existing
25
+ associations.
26
+
27
+ == First Public Release Notice!
28
+
29
+ This is the first public release of this gem, so while it has been kind to us
30
+ and our software, we can't guarantee that for everyone. That said, we do hope
31
+ that someone out there finds it useful and enjoys the new-found powers of
32
+ PostgreSQL!
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+
2
+ # -*- ruby -*-
3
+
4
+ require 'rubygems'
5
+ require 'rubygems/package_task'
6
+ require 'rake/testtask'
7
+ require 'rdoc/task'
8
+
9
+ $:.push 'lib'
10
+
11
+ version = File.read('VERSION') rescue ''
12
+
13
+ begin
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ gem.name = "activerecord-postgresql-extensions"
17
+ gem.summary = "A whole bunch of extensions the ActiveRecord PostgreSQL adapter."
18
+ gem.description = gem.summary
19
+ gem.email = "code@zoocasa.com"
20
+ gem.homepage = "http://github.com/zoocasa/activerecord-postgresql-extensions"
21
+ gem.authors = [ "J Smith" ]
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
26
+ end
27
+
28
+ desc 'Test PostgreSQL extensions'
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = false
32
+ end
33
+
34
+ desc 'Build docs'
35
+ Rake::RDocTask.new do |t|
36
+ require 'rdoc'
37
+ t.title = "ActiveRecord PostgreSQL Extensions #{version}"
38
+ t.main = 'README.rdoc'
39
+ t.rdoc_dir = 'doc'
40
+ t.rdoc_files.include('README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb')
41
+ end
42
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.7
@@ -0,0 +1,30 @@
1
+
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
4
+ module PostgreSQLExtensions
5
+ end
6
+
7
+ dirname = File.join(File.dirname(__FILE__), 'postgresql_extensions')
8
+
9
+ %w{
10
+ postgresql_adapter_extensions
11
+ postgresql_constraints
12
+ postgresql_tables
13
+ postgresql_indexes
14
+ postgresql_permissions
15
+ postgresql_schemas
16
+ postgresql_languages
17
+ postgresql_rules
18
+ postgresql_functions
19
+ postgresql_sequences
20
+ postgresql_triggers
21
+ postgresql_views
22
+ postgresql_geometry
23
+ postgresql_types
24
+ foreign_key_associations
25
+ }.each do |file|
26
+ require File.join(dirname, file)
27
+ end
28
+
29
+ ActiveRecord::Base.send(:include, PostgreSQLExtensions::ActiveRecord::ForeignKeyAssociations)
30
+
@@ -0,0 +1,367 @@
1
+
2
+ module PostgreSQLExtensions::ActiveRecord
3
+ # The ForeignKeyAssociations module attempts to automatically create
4
+ # associations based on your database schema by looking at foreign key
5
+ # relationships. It can be enabled by setting the
6
+ # enable_foreign_key_associations configuration option on
7
+ # ActiveRecord::Base to true.
8
+ #
9
+ # The ForeignKeyAssociations isn't a replacement for hand-coded
10
+ # associations, as it specifically won't override any associations you
11
+ # create in your models, but can serve to keep your models a little more
12
+ # up-to-date by using the database itself as a means to creating
13
+ # associations.
14
+ #
15
+ # Foreign key associations are formed by looking at various system tables
16
+ # in your database and attempting to make sane decisions based on how
17
+ # foreign key relationships and indexes are created. We basically go by the
18
+ # following rules:
19
+ #
20
+ # * a foreign key reference will create a belongs_to on the model doing
21
+ # the referencing as well as either a has_one or has_many association
22
+ # on the referenced table. If there is a UNIQUE index on the foreign key
23
+ # column, we use a has_one association; otherwise, we use a has_many.
24
+ # * "has_many :through" associations are found using multi-column UNIQUE
25
+ # indexes and existing associations which are either found during the
26
+ # first stages of our process or are pre-existing.
27
+ #
28
+ # Using PostgreSQL as an example:
29
+ #
30
+ # CREATE TABLE "foos" (
31
+ # id serial NOT NULL PRIMARY KEY
32
+ # );
33
+ #
34
+ # CREATE TABLE "bars" (
35
+ # id serial NOT NULL PRIMARY KEY,
36
+ # foo_id integer NOT NULL REFERENCES "foo"("id")
37
+ # );
38
+ #
39
+ # In this case, we will attempt to create the following associations:
40
+ #
41
+ # # Foo model:
42
+ # has_many :bars
43
+ #
44
+ # # Bar model:
45
+ # belongs_to :foo
46
+ #
47
+ # If we were to add a UNIQUE index on the foo_id column in bars, we
48
+ # would get a has_one assocation in the foos model:
49
+ #
50
+ # CREATE TABLE "bars" (
51
+ # id serial NOT NULL PRIMARY KEY,
52
+ # foo_id integer NOT NULL REFERENCES "foo"("id") UNIQUE
53
+ # );
54
+ # # or ALTER TABLE or CREATE UNIQUE INDEX, whatever
55
+ #
56
+ # Produces the following associations:
57
+ #
58
+ # # Foo model:
59
+ # has_one :bars
60
+ #
61
+ # # Bar model:
62
+ # belongs_to :foo
63
+ #
64
+ # We also attempt to do "has_many :through" associations by looking for
65
+ # things like UNIQUE indexes on multiple columns, previously existing
66
+ # associations and model names. For instance, given the following
67
+ # schema:
68
+ #
69
+ # CREATE TABLE "foos" (
70
+ # id serial NOT NULL PRIMARY KEY
71
+ # );
72
+ #
73
+ # CREATE TABLE "bars" (
74
+ # id serial NOT NULL PRIMARY KEY
75
+ # );
76
+ #
77
+ # CREATE TABLE "foo_bars" (
78
+ # id serial NOT NULL PRIMARY KEY,
79
+ # foo_id integer NOT NULL REFERENCES "foos"("id"),
80
+ # bar_id integer NOT NULL REFERENCES "bars"("id"),
81
+ # UNIQUE ("foo_id", "bar_id")
82
+ # );
83
+ #
84
+ # Would create the following associations:
85
+ #
86
+ # # FooBar model:
87
+ # belongs_to :foo
88
+ # belongs_to :bar
89
+ #
90
+ # # Foo model:
91
+ # has_many :foo_bars
92
+ # has_many :bars, :through => :foo_bars
93
+ #
94
+ # # Bar model:
95
+ # has_many :foo_bars
96
+ # has_many :foos, :through => :foo_bars
97
+ #
98
+ # The rules for association creation through foreign keys are fairly lax,
99
+ # i.e. you don't need to name your keys "something_id" as Rails generally
100
+ # demands by default. About the only thing that would really help us find
101
+ # foreign key associations is the naming used by your models: if they
102
+ # don't match up with the Rails conventions for model-to-table mapping
103
+ # (pluralization, underscores, etc.), we can get confused and may miss
104
+ # some associations. The associations will eventually be created once
105
+ # all of your models are loaded, but as of Rails 2.0 we can't guarantee
106
+ # when and if all of your models will load before we try to find our
107
+ # foreign keys, so bear that in mind when using this plugin. The only time
108
+ # this really comes up is when you're using set_table_name in a model to
109
+ # override the Rails conventions and we can't figure that out during our
110
+ # foreign key hunt.
111
+ #
112
+ # Note that this plugin will never try to override existing associations.
113
+ # If you have an existing association with the same name as one that we
114
+ # are trying to create (or for that matter, a method with the same name)
115
+ # then we will just silently and happily skip that association.
116
+ #
117
+ # Portions of this plugin were inspired by the RedHill on Rails plugins
118
+ # available at http://www.redhillonrails.org/. The idea is basically
119
+ # the same in both cases, although our implementations are rather
120
+ # different both in terms of structure and functionality, as this plugin
121
+ # is more specific to our particular needs.
122
+ module ForeignKeyAssociations
123
+ def self.included(base)
124
+ base.extend(ClassMethods)
125
+ end
126
+
127
+ module ClassMethods
128
+ def self.extended(base)
129
+ class << base
130
+ alias_method_chain :allocate, :foreign_keys
131
+ alias_method_chain :new, :foreign_keys
132
+ alias_method_chain :reflections, :foreign_keys
133
+ end
134
+ end
135
+
136
+ def allocate_with_foreign_keys #:nodoc:
137
+ load_foreign_key_associations if load_foreign_key_associations? && !@foreign_key_associations_loaded
138
+ allocate_without_foreign_keys
139
+ end
140
+
141
+ def new_with_foreign_keys(*args) #:nodoc:
142
+ load_foreign_key_associations if load_foreign_key_associations? && !@foreign_key_associations_loaded
143
+ new_without_foreign_keys(*args) { |*block_args|
144
+ yield(*block_args) if block_given?
145
+ }
146
+ end
147
+
148
+ def reflections_with_foreign_keys #:nodoc:
149
+ load_foreign_key_associations if load_foreign_key_associations? && !@foreign_key_associations_loaded
150
+ reflections_without_foreign_keys
151
+ end
152
+
153
+ # Returns an Array of foreign keys referencing this model. See
154
+ # ActiveRecord::Base#referenced_foreign_keys for details.
155
+ def referenced_foreign_keys
156
+ @referenced_foreign_keys ||= connection.referenced_foreign_keys(table_name, "#{name} Referenced Foreign Keys")
157
+ end
158
+
159
+ # Returns an Array of foreign keys in this model. See
160
+ # ActiveRecord::Base#foreign_keys for details.
161
+ def foreign_keys
162
+ @foreign_keys ||= connection.foreign_keys(table_name, "#{name} Foreign Keys")
163
+ end
164
+
165
+ # Allows you to selectively disable foreign key association loading
166
+ # when the ActiveRecord setting enable_foreign_key_associations
167
+ # is enabled. This works on a per-model basis, and prevents any
168
+ # foreign key associations from being created on this model. This
169
+ # applies to both foreign keys that reference this model as well
170
+ # as foreign keys within the model itself.
171
+ def dont_load_foreign_key_associations!
172
+ @load_foreign_key_associations = false
173
+ end
174
+
175
+ # Creates foreign key associations for the model. This is
176
+ # essentially a three-step process:
177
+ #
178
+ # 1. Find any tables that reference this model via foreign keys
179
+ # and create the associations accordingly.
180
+ # 2. Find any foreign keys in this model and create the
181
+ # associations accordingly. This process creates both belongs_to
182
+ # associations on this model to the referenced models as well
183
+ # as has_many/has_one associations on the referenced models
184
+ # themselves. To determine whether the association is a has_many
185
+ # or a has_one, we take a look at UNIQUE indexes created on the
186
+ # table column. In cases where the index is UNIQUE, we create
187
+ # a has_one association; in all others, we create a has_many
188
+ # association.
189
+ # 3. Look at the model itself and try to determine whether or not
190
+ # we have a "has_many :through" association. We make this
191
+ # determination by looking to see if there are two foreign
192
+ # keys with the following conditions:
193
+ # * the model has an index with exactly two columns in it and
194
+ # the index itself is UNIQUE;
195
+ # * we've already created a belongs_to association with each
196
+ # column and the column names match the columns in the UNIQUE
197
+ # index; and
198
+ # * the model name is either "FirstModelSecondModel" or
199
+ # "SecondModelFirstModel".
200
+ # If these criteria match, then the "has_many :through"
201
+ # associations are created on both of the referenced models.
202
+ #
203
+ # In all cases, we respect any dont_load_foreign_key_associations!
204
+ # settings on individual models as well as take into account
205
+ # existing associations with the same names as the ones we're going
206
+ # to try to create. In other words, if you already have an
207
+ # association called :listings on a model and we find a foreign
208
+ # key that matches, we won't blow away your existing association
209
+ # and instead just continue along merrily.
210
+ def load_foreign_key_associations
211
+ return if @foreign_key_associations_loaded
212
+ @foreign_key_associations_loaded = true
213
+
214
+ indexes = connection.indexes(table_name, "#{name} Indexes")
215
+
216
+ # This does the associations for the tables that reference
217
+ # columns in this table.
218
+ referenced_foreign_keys.each do |fk|
219
+ begin
220
+ referencing_class = compute_type(fk[:table].classify)
221
+ referencing_class.load_foreign_key_associations if referencing_class.load_foreign_key_associations?
222
+ rescue NameError
223
+ # Do nothing. We won't bother creating associations
224
+ # if the model class itself doesn't exist.
225
+ end
226
+ end
227
+
228
+ # This does the foreign key associations for this model.
229
+ foreign_keys.each do |fk|
230
+ belongs_to_association_id = fk[:table].singularize.to_sym
231
+ begin
232
+ references_class_name = fk[:table].classify
233
+ references_class = compute_type(references_class_name)
234
+
235
+ unless method_defined?(belongs_to_association_id)
236
+ belongs_to(
237
+ belongs_to_association_id,
238
+ :class_name => references_class_name,
239
+ :foreign_key => fk[:column]
240
+ )
241
+ end
242
+
243
+ # If we have a unique index for this column, we'll
244
+ # create a has_one association; otherwise, it's a
245
+ # has_many.
246
+ if indexes.detect { |i|
247
+ i.columns.length == 1 && i.unique && i.columns.include?(fk[:column])
248
+ }
249
+ has_association_id = self.name.demodulize.underscore.to_sym
250
+ unless references_class.method_defined?(has_association_id)
251
+ references_class.has_one(
252
+ has_association_id, {
253
+ :class_name => name,
254
+ :foreign_key => fk[:column]
255
+ }
256
+ )
257
+ end
258
+ else
259
+ has_association_id = self.name.demodulize.underscore.pluralize.to_sym
260
+ unless references_class.method_defined?(has_association_id)
261
+ references_class.has_many(
262
+ has_association_id, {
263
+ :class_name => name,
264
+ :foreign_key => fk[:column]
265
+ }
266
+ )
267
+ end
268
+ end
269
+ rescue NameError
270
+ # Do nothing. NOTHING! We don't want to create
271
+ # associations on non-existent classes.
272
+ end
273
+ end
274
+
275
+ # If we have an index that contains exactly two columns and
276
+ # it's a UNIQUE index, then we might have a
277
+ # "has_many :through" association, so let's look for it now.
278
+ if through = indexes.detect { |i| i.columns.length == 2 && i.unique }
279
+ catch :not_a_has_many_through do
280
+ hmt_associations = []
281
+
282
+ # This will loop through the columns in the UNIQUE
283
+ # index and see if they're both foreign keys
284
+ # referencing other tables.
285
+ through.columns.each do |c|
286
+ if foreign_keys.detect { |fk| fk[1] == c }.blank?
287
+ throw(:not_a_has_many_through)
288
+ end
289
+
290
+ # Check that both columns have belongs_to
291
+ # associations.
292
+ unless hmt_association = reflections.detect { |r, v|
293
+ v.macro == :belongs_to && v.primary_key_name == c
294
+ }
295
+ throw(:not_a_has_many_through)
296
+ end
297
+
298
+ hmt_associations << hmt_association
299
+ end
300
+
301
+ hmt_first = hmt_associations.first
302
+ hmt_second = hmt_associations.last
303
+
304
+ hmt_first_association_id = hmt_second.first.to_s.pluralize.to_sym
305
+ hmt_second_association_id = hmt_first.first.to_s.pluralize.to_sym
306
+
307
+ hmt_first_class = hmt_first.last.name.constantize
308
+ hmt_second_class = hmt_second.last.name.constantize
309
+
310
+ # Check to see if this model is named
311
+ # "FirstModelSecondModel" or "SecondModelFirstModel".
312
+ if strict_foreign_key_has_many_throughs
313
+ unless [
314
+ "#{hmt_first_class}#{hmt_second_class}",
315
+ "#{hmt_second_class}#{hmt_first_class}"
316
+ ].include?(self.name)
317
+ throw(:not_a_has_many_through)
318
+ end
319
+ end
320
+
321
+ # If we haven't thrown up, we can create the
322
+ # associations, assuming they don't already exist and
323
+ # we're allowed to.
324
+ through_association_id = self.name.demodulize.underscore.pluralize.to_sym
325
+
326
+ if hmt_first_class.load_foreign_key_associations?
327
+ unless hmt_first_class.method_defined?(hmt_first_association_id)
328
+ hmt_first_class.has_many(
329
+ hmt_first_association_id,
330
+ :through => through_association_id
331
+ )
332
+ end
333
+ end
334
+
335
+ if hmt_second_class.load_foreign_key_associations?
336
+ unless hmt_second_class.method_defined?(hmt_second_association_id)
337
+ hmt_second_class.has_many(
338
+ hmt_second_association_id,
339
+ :through => through_association_id
340
+ )
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ # Should we load a model's foreign key associations? Maybe we
348
+ # should, and maybe we shouldn't.
349
+ def load_foreign_key_associations?
350
+ ActiveRecord::Base.enable_foreign_key_associations &&
351
+ !abstract_class? && (
352
+ @load_foreign_key_associations.nil? || @load_foreign_key_associations
353
+ )
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ ActiveRecord::Base.class_eval do
360
+ # Enable foreign key associations.
361
+ cattr_accessor :enable_foreign_key_associations
362
+
363
+ # Be a bit stricter when looking for "has_many :through" associations by
364
+ # checking the name of the through model for the Rails-like naming
365
+ # convention of "FirstModelSecondModel".
366
+ cattr_accessor :strict_foreign_key_has_many_throughs
367
+ end