activerecord-postgresql-extensions 0.0.7

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.
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