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