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
@@ -0,0 +1,131 @@
1
+
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
4
+ module ActiveRecord
5
+ class InvalidTriggerCallType < ActiveRecordError #:nodoc:
6
+ def initialize(called)
7
+ super("Invalid trigger call type - #{called}")
8
+ end
9
+ end
10
+
11
+ class InvalidTriggerEvents < ActiveRecordError #:nodoc:
12
+ def initialize(events)
13
+ super("Invalid trigger event(s) - #{events.inspect}")
14
+ end
15
+ end
16
+
17
+ module ConnectionAdapters
18
+ class PostgreSQLAdapter < AbstractAdapter
19
+ # Creates a PostgreSQL trigger.
20
+ #
21
+ # The +called+ argument specifies when the trigger is called and
22
+ # can be either <tt>:before</tt> or <tt>:after</tt>.
23
+ #
24
+ # +events+ can be on or more of <tt>:insert</tt>,
25
+ # <tt>:update</tt> or <tt>:delete</tt>. There are no
26
+ # <tt>:select</tt> triggers, as SELECT generally doesn't modify
27
+ # the database.
28
+ #
29
+ # +table+ is obviously the table the trigger will be created on
30
+ # while +function+ is the name of the procedure to call when the
31
+ # trigger is fired.
32
+ #
33
+ # ==== Options
34
+ #
35
+ # * <tt>:for_each</tt> - defines whether the trigger will be fired
36
+ # on each row in a statement or on the statement itself. Possible
37
+ # values are <tt>:row</tt> and <tt>:statement</tt>, with
38
+ # <tt>:statement</tt> being the default.
39
+ # * <tt>:args</tt> - if the trigger function requires any
40
+ # arguments then this is the place to let everyone know about it.
41
+ #
42
+ # ==== Example
43
+ #
44
+ # ### ruby
45
+ # create_trigger(
46
+ # 'willie_nelsons_trigger',
47
+ # :before,
48
+ # :update,
49
+ # { :nylon => :guitar },
50
+ # 'strum_trigger',
51
+ # :for_each => :row
52
+ # )
53
+ # # => CREATE TRIGGER "willie_nelsons_trigger" BEFORE UPDATE
54
+ # # ON "nylon"."guitar" FOR EACH ROW EXECUTE PROCEDURE "test_trigger"();
55
+ def create_trigger(name, called, events, table, function, options = {})
56
+ execute PostgreSQLTriggerDefinition.new(self, name, called, events, table, function, options).to_s
57
+ end
58
+
59
+ # Drops a trigger.
60
+ #
61
+ # ==== Options
62
+ #
63
+ # * <tt>:if_exists</tt> - adds IF EXISTS.
64
+ # * <tt>:cascade</tt> - cascades changes down to objects referring
65
+ # to the trigger.
66
+ def drop_trigger(name, table, options = {})
67
+ sql = 'DROP TRIGGER '
68
+ sql << 'IF EXISTS ' if options[:if_exists]
69
+ sql << "#{quote_generic(name)} ON #{quote_table_name(table)}"
70
+ sql << ' CASCADE' if options[:cascade]
71
+ execute sql
72
+ end
73
+
74
+ # Renames a trigger.
75
+ def rename_trigger(name, table, new_name, options = {})
76
+ execute "ALTER TRIGGER #{quote_generic(name)} ON #{quote_table_name(table)} RENAME TO #{quote_generic(new_name)}"
77
+ end
78
+ end
79
+
80
+ # Creates a PostgreSQL trigger definition. This class isn't really
81
+ # meant to be used directly. You'd be better off sticking to
82
+ # PostgreSQLAdapter#create_trigger. Honestly.
83
+ class PostgreSQLTriggerDefinition
84
+ attr_accessor :base, :name, :called, :events, :table, :function, :options
85
+
86
+ def initialize(base, name, called, events, table, function, options = {}) #:nodoc:
87
+ assert_valid_called(called)
88
+ assert_valid_events(events)
89
+ assert_valid_for_each(options[:for_each])
90
+
91
+ @base, @name, @events, @called, @table, @function, @options =
92
+ base, name, events, called, table, function, options
93
+ end
94
+
95
+ def to_sql #:nodoc:
96
+ sql = "CREATE TRIGGER #{base.quote_generic(name)} #{called.to_s.upcase} "
97
+ sql << Array(events).collect { |e| e.to_s.upcase }.join(' OR ')
98
+ sql << " OF " << Array(options[:of]).collect { |o| base.quote_generic(o) }.join(', ') if options[:of].present?
99
+ sql << " ON #{base.quote_table_name(table)}"
100
+ sql << " FOR EACH #{options[:for_each].to_s.upcase}" if options[:for_each]
101
+ sql << " EXECUTE PROCEDURE #{base.quote_function(function)}(#{options[:args]})"
102
+ sql
103
+ end
104
+ alias :to_s :to_sql
105
+
106
+ private
107
+ CALLED_TYPES = %w{ before after }.freeze
108
+ EVENT_TYPES = %w{ insert update delete }.freeze
109
+ FOR_EACH_TYPES = %w{ row statement }.freeze
110
+
111
+ def assert_valid_called(c) #:nodoc:
112
+ if !CALLED_TYPES.include?(c.to_s.downcase)
113
+ raise ActiveRecord::InvalidTriggerCallType.new(c)
114
+ end
115
+ end
116
+
117
+ def assert_valid_events(events) #:nodoc:
118
+ check_events = Array(events).collect(&:to_s) - EVENT_TYPES
119
+ if !check_events.empty?
120
+ raise ActiveRecord::InvalidTriggerEvent.new(check_events)
121
+ end
122
+ end
123
+
124
+ def assert_valid_for_each(f) #:nodoc:
125
+ if !FOR_EACH_TYPES.include?(f.to_s.downcase)
126
+ raise ActiveRecord::InvalidTriggerForEach.new(f)
127
+ end unless f.nil?
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,17 @@
1
+
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ class PostgreSQLAdapter < AbstractAdapter
7
+ # Returns an Array of available languages.
8
+ def types(name = nil)
9
+ query(%{SELECT typname FROM pg_type}, name).map { |row| row[0] }
10
+ end
11
+
12
+ def type_exists?(name)
13
+ types.include?(name.to_s)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,103 @@
1
+
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ class PostgreSQLAdapter < AbstractAdapter
7
+ # Creates a new PostgreSQL view.
8
+ #
9
+ # +name+ is the name of the view. View quoting works the same as
10
+ # table quoting, so you can use PostgreSQLAdapter#with_schema and
11
+ # friends. See PostgreSQLAdapter#with_schema and
12
+ # PostgreSQLAdapter#quote_table_name for details.
13
+ #
14
+ # +query+ is the SELECT query to use for the view. This is just
15
+ # a straight-up String, so quoting rules will not apply.
16
+ #
17
+ # Note that you can grant privileges on views using the
18
+ # grant_view_privileges method and revoke them using
19
+ # revoke_view_privileges.
20
+ #
21
+ # ==== Options
22
+ #
23
+ # * <tt>:replace</tt> - adds a REPLACE clause, as in "CREATE OR
24
+ # REPLACE".
25
+ # * <tt>:temporary</tt> - adds a TEMPORARY clause.
26
+ # * <tt>:columns</tt> - you can rename the output columns as
27
+ # necessary. Note that this can be an Array and that it must be
28
+ # the same length as the number of output columns created by
29
+ # +query+.
30
+ #
31
+ # ==== Examples
32
+ #
33
+ # ### ruby
34
+ # create_view(:foo_view, 'SELECT * FROM bar')
35
+ # # => CREATE VIEW "foo_view" AS SELECT * FROM bar;
36
+ #
37
+ # create_view(
38
+ # { :geospatial => :foo_view },
39
+ # 'SELECT * FROM bar',
40
+ # :columns => [ :id, :name, :the_geom ]
41
+ # )
42
+ # # => CREATE VIEW "geospatial"."foo_view" ("id", "name", "the_geom") AS SELECT * FROM bar;
43
+ def create_view(name, query, options = {})
44
+ execute PostgreSQLViewDefinition.new(self, name, query, options).to_s
45
+ end
46
+
47
+ # Drops a view.
48
+ #
49
+ # ==== Options
50
+ #
51
+ # * <tt>:if_exists</tt> - adds IF EXISTS.
52
+ # * <tt>:cascade</tt> - adds CASCADE.
53
+ def drop_view(name, options = {})
54
+ sql = 'DROP VIEW '
55
+ sql << 'IF EXISTS ' if options[:if_exists]
56
+ sql << Array(name).collect { |v| quote_view_name(v) }.join(', ')
57
+ sql << ' CASCADE' if options[:cascade]
58
+ execute sql
59
+ end
60
+
61
+ # Renames a view.
62
+ def rename_view(name, new_name, options = {})
63
+ execute "ALTER TABLE #{quote_view_name(name)} RENAME TO #{quote_generic_ignore_schema(new_name)}"
64
+ end
65
+
66
+ # Change the ownership of a view.
67
+ def alter_view_owner(name, role, options = {})
68
+ execute "ALTER TABLE #{quote_view_name(name)} OWNER TO #{quote_role(role)}"
69
+ end
70
+
71
+ # Alter a view's schema.
72
+ def alter_view_schema(name, schema, options = {})
73
+ execute "ALTER TABLE #{quote_view_name(name)} SET SCHEMA #{quote_schema(schema)}"
74
+ end
75
+ end
76
+
77
+ # Creates a PostgreSQL view definition. This class isn't really meant
78
+ # to be used directly. Instead, see PostgreSQLAdapter#create_view
79
+ # for usage.
80
+ class PostgreSQLViewDefinition
81
+ attr_accessor :base, :name, :query, :options
82
+
83
+ def initialize(base, name, query, options = {}) #:nodoc:
84
+ @base, @name, @query, @options = base, name, query, options
85
+ end
86
+
87
+ def to_sql #:nodoc:
88
+ sql = 'CREATE '
89
+ sql << 'OR REPLACE ' if options[:replace]
90
+ sql << 'TEMPORARY ' if options[:temporary]
91
+ sql << "VIEW #{base.quote_view_name(name)} "
92
+ if options[:columns]
93
+ sql << '(' << Array(options[:columns]).collect do |c|
94
+ base.quote_column_name(c)
95
+ end.join(', ') << ') '
96
+ end
97
+ sql << "AS #{query}"
98
+ sql
99
+ end
100
+ alias :to_s :to_sql
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,50 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{postgresql-extensions}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["J Smith"]
12
+ s.date = %q{2010-11-30}
13
+ s.description = %q{A whole bunch of extensions the Rails PostgreSQL adapter.}
14
+ s.email = %q{code@zoocasa.com}
15
+ s.extra_rdoc_files = [
16
+ "README"
17
+ ]
18
+ s.files = [
19
+ "README",
20
+ "Rakefile",
21
+ "lib/postgresql_adapter_extensions.rb",
22
+ "lib/postgresql_constraints.rb",
23
+ "lib/postgresql_functions.rb",
24
+ "lib/postgresql_geometry.rb",
25
+ "lib/postgresql_indexes.rb",
26
+ "lib/postgresql_languages.rb",
27
+ "lib/postgresql_permissions.rb",
28
+ "lib/postgresql_rules.rb",
29
+ "lib/postgresql_schemas.rb",
30
+ "lib/postgresql_sequences.rb",
31
+ "lib/postgresql_tables.rb",
32
+ "lib/postgresql_triggers.rb",
33
+ "lib/postgresql_views.rb"
34
+ ]
35
+ s.homepage = %q{http://github.com/zoocasa/postgresql-extensions}
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = %q{1.3.7}
38
+ s.summary = %q{A whole bunch of extensions the Rails PostgreSQL adapter.}
39
+
40
+ if s.respond_to? :specification_version then
41
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ else
46
+ end
47
+ else
48
+ end
49
+ end
50
+
@@ -0,0 +1,45 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class AdapterExtensionTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_quote_table_name_with_schema_string
9
+ assert_equal(%{"foo"."bar"}, ARBC.quote_table_name('foo.bar'))
10
+ end
11
+
12
+ def test_quote_table_name_with_schema_hash
13
+ assert_equal(%{"foo"."bar"}, ARBC.quote_table_name(:foo => :bar))
14
+ end
15
+
16
+ def test_quote_table_name_with_current_schema
17
+ assert_equal(%{"foo"."bar"}, ARBC.with_schema(:foo) {
18
+ ARBC.quote_table_name(:bar)
19
+ })
20
+ end
21
+
22
+ def test_quote_table_name_with_current_schema_ignored
23
+ assert_equal(%{"bar"}, ARBC.with_schema(:foo) {
24
+ ARBC.ignore_schema {
25
+ ARBC.quote_table_name(:bar)
26
+ }
27
+ })
28
+ end
29
+
30
+ def test_quote_schema
31
+ assert_equal('PUBLIC', ARBC.quote_schema(:public))
32
+ assert_equal(%{"foo"}, ARBC.quote_schema(:foo))
33
+ end
34
+
35
+ def test_other_quoting
36
+ assert_equal(%{"foo"}, ARBC.quote_generic(:foo))
37
+ assert_equal(%{"foo"}, ARBC.quote_role(:foo))
38
+ assert_equal(%{"foo"}, ARBC.quote_rule(:foo))
39
+ assert_equal(%{"foo"}, ARBC.quote_language(:foo))
40
+ assert_equal(%{"foo"}, ARBC.quote_sequence(:foo))
41
+ assert_equal(%{"foo"}, ARBC.quote_function(:foo))
42
+ assert_equal(%{"foo"}, ARBC.quote_view_name(:foo))
43
+ assert_equal(%{"foo"}, ARBC.quote_tablespace(:foo))
44
+ end
45
+ end
@@ -0,0 +1,98 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class ConstraintTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def setup
9
+ clear_statements!
10
+ end
11
+
12
+ def test_create_table_with_unique_constraint_on_table
13
+ Mig.create_table(:foo) do |t|
14
+ t.integer :bar_id
15
+ t.text :name
16
+ t.text :email
17
+ t.unique_constraint [ :id, :bar_id ]
18
+ t.unique_constraint [ :name, :email ], :tablespace => 'fubar'
19
+ end
20
+
21
+ assert_equal((<<-EOF).strip, statements[0])
22
+ CREATE TABLE "foo" (
23
+ "id" serial primary key,
24
+ "bar_id" integer,
25
+ "name" text,
26
+ "email" text,
27
+ UNIQUE ("id", "bar_id"),
28
+ UNIQUE ("name", "email") USING INDEX TABLESPACE "fubar"
29
+ )
30
+ EOF
31
+ end
32
+
33
+ def test_create_table_with_unique_constraint_on_column
34
+ Mig.create_table(:foo) do |t|
35
+ t.integer :bar_id, :unique => true
36
+ end
37
+
38
+ assert_equal((<<-EOF).strip, statements[0])
39
+ CREATE TABLE "foo" (
40
+ "id" serial primary key,
41
+ "bar_id" integer,
42
+ UNIQUE ("bar_id")
43
+ )
44
+ EOF
45
+ end
46
+
47
+ def test_add_unique_constraint
48
+ Mig.add_unique_constraint(:foo, :bar_id)
49
+ Mig.add_unique_constraint(
50
+ :foo,
51
+ :bar_id,
52
+ :tablespace => 'fubar',
53
+ :storage_parameters => 'FILLFACTOR=10',
54
+ :name => 'bar_id_unique'
55
+ )
56
+
57
+ assert_equal([
58
+ "ALTER TABLE \"foo\" ADD UNIQUE (\"bar_id\")",
59
+ "ALTER TABLE \"foo\" ADD CONSTRAINT \"bar_id_unique\" UNIQUE (\"bar_id\") WITH (FILLFACTOR=10) USING INDEX TABLESPACE \"fubar\""
60
+ ], statements)
61
+ end
62
+
63
+ def test_add_foreign_key
64
+ Mig.add_foreign_key(:foo, :bar_id, :bar)
65
+ Mig.add_foreign_key(:foo, :bar_id, :bar, :ogc_fid, :name => 'bar_fk')
66
+ Mig.add_foreign_key(:foo, [ :one_id, :bar_id ], :bar, [ :one_id, :bar_id ], :match => 'full')
67
+ Mig.add_foreign_key(:foo, :bar_id, :bar, :on_delete => :cascade, :on_delete => :set_default)
68
+ Mig.add_foreign_key(:foo, :bar_id, :bar, :deferrable => :immediate)
69
+
70
+ assert_equal([
71
+ "ALTER TABLE \"foo\" ADD FOREIGN KEY (\"bar_id\") REFERENCES \"bar\"",
72
+ "ALTER TABLE \"foo\" ADD CONSTRAINT \"bar_fk\" FOREIGN KEY (\"bar_id\") REFERENCES \"bar\" (\"ogc_fid\")",
73
+ "ALTER TABLE \"foo\" ADD FOREIGN KEY (\"one_id\", \"bar_id\") REFERENCES \"bar\" (\"one_id\", \"bar_id\") MATCH FULL",
74
+ "ALTER TABLE \"foo\" ADD FOREIGN KEY (\"bar_id\") REFERENCES \"bar\" ON DELETE SET DEFAULT",
75
+ "ALTER TABLE \"foo\" ADD FOREIGN KEY (\"bar_id\") REFERENCES \"bar\" DEFERRABLE INITIALLY IMMEDIATE"
76
+ ], statements)
77
+ end
78
+
79
+ def test_drop_constraint
80
+ Mig.drop_constraint(:foo, :bar)
81
+ Mig.drop_constraint(:foo, :bar, :cascade => true)
82
+
83
+ assert_equal([
84
+ "ALTER TABLE \"foo\" DROP CONSTRAINT \"bar\"",
85
+ "ALTER TABLE \"foo\" DROP CONSTRAINT \"bar\" CASCADE"
86
+ ], statements)
87
+ end
88
+
89
+ def test_add_check_constraint
90
+ Mig.add_check_constraint(:foo, 'length(name) < 100')
91
+ Mig.add_check_constraint(:foo, 'length(name) < 100', :name => 'name_length_check')
92
+
93
+ assert_equal([
94
+ "ALTER TABLE \"foo\" ADD CHECK (length(name) < 100)",
95
+ "ALTER TABLE \"foo\" ADD CONSTRAINT \"name_length_check\" CHECK (length(name) < 100)"
96
+ ], statements)
97
+ end
98
+ end