pg_power 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/README.markdown +212 -0
  2. data/lib/core_ext/active_record/connection_adapters/abstract/schema_statements.rb +139 -0
  3. data/lib/core_ext/active_record/connection_adapters/postgresql_adapter.rb +135 -0
  4. data/lib/core_ext/active_record/schema_dumper.rb +40 -0
  5. data/lib/pg_power.rb +16 -0
  6. data/lib/pg_power/connection_adapters.rb +9 -0
  7. data/lib/pg_power/connection_adapters/abstract_adapter.rb +20 -0
  8. data/lib/pg_power/connection_adapters/abstract_adapter/comment_methods.rb +62 -0
  9. data/lib/pg_power/connection_adapters/abstract_adapter/foreigner_methods.rb +67 -0
  10. data/lib/pg_power/connection_adapters/abstract_adapter/index_methods.rb +6 -0
  11. data/lib/pg_power/connection_adapters/abstract_adapter/schema_methods.rb +18 -0
  12. data/lib/pg_power/connection_adapters/foreign_key_definition.rb +5 -0
  13. data/lib/pg_power/connection_adapters/index_definition.rb +6 -0
  14. data/lib/pg_power/connection_adapters/postgresql_adapter.rb +16 -0
  15. data/lib/pg_power/connection_adapters/postgresql_adapter/comment_methods.rb +79 -0
  16. data/lib/pg_power/connection_adapters/postgresql_adapter/foreigner_methods.rb +190 -0
  17. data/lib/pg_power/connection_adapters/postgresql_adapter/index_methods.rb +42 -0
  18. data/lib/pg_power/connection_adapters/postgresql_adapter/schema_methods.rb +22 -0
  19. data/lib/pg_power/connection_adapters/table.rb +17 -0
  20. data/lib/pg_power/connection_adapters/table/comment_methods.rb +58 -0
  21. data/lib/pg_power/connection_adapters/table/foreigner_methods.rb +51 -0
  22. data/lib/pg_power/engine.rb +46 -0
  23. data/lib/pg_power/migration.rb +4 -0
  24. data/lib/pg_power/migration/command_recorder.rb +13 -0
  25. data/lib/pg_power/migration/command_recorder/comment_methods.rb +52 -0
  26. data/lib/pg_power/migration/command_recorder/foreigner_methods.rb +29 -0
  27. data/lib/pg_power/migration/command_recorder/schema_methods.rb +39 -0
  28. data/lib/pg_power/schema_dumper.rb +21 -0
  29. data/lib/pg_power/schema_dumper/comment_methods.rb +36 -0
  30. data/lib/pg_power/schema_dumper/foreigner_methods.rb +58 -0
  31. data/lib/pg_power/schema_dumper/schema_methods.rb +51 -0
  32. data/lib/pg_power/tools.rb +56 -0
  33. data/lib/pg_power/version.rb +4 -0
  34. data/lib/tasks/pg_power_tasks.rake +4 -0
  35. metadata +213 -0
@@ -0,0 +1,212 @@
1
+ # PgPower
2
+
3
+ ActiveRecord extension to get more from PostgreSQL:
4
+
5
+ * Create/drop schemas.
6
+ * Set/remove comments on columns and tables.
7
+ * Use foreign keys.
8
+ * Use partial indexes.
9
+
10
+ ## Environment notes
11
+
12
+ It was tested with Rails 3.1.x and 3.2.x, Ruby 1.8.7 REE and 1.9.3.
13
+
14
+
15
+ ## Schemas
16
+
17
+ ### Create schema
18
+
19
+ In migrations you can use `create_schema` and `drop_schema` methods like this:
20
+
21
+ class ReplaceDemographySchemaWithPolitics < ActiveRecord::Migration
22
+ def change
23
+ drop_schema 'demography'
24
+ create_schema 'politics'
25
+ end
26
+ end
27
+
28
+ ### Create table
29
+
30
+ Use schema `:schema` option to specify schema name:
31
+
32
+ create_table "countries", :schema => "demography" do |t|
33
+ # columns goes here
34
+ end
35
+
36
+ ### Move table to another schema
37
+
38
+ Move table `countries` from `demography` schema to `public`:
39
+
40
+ move_table_to_schema 'demography.countries', :public
41
+
42
+ ## Table and column comments
43
+
44
+ Provides the following methods to manage comments:
45
+
46
+ * set\_table\_comment(table\_name, comment)
47
+ * remove\_table\_comment(table\_name)
48
+ * set\_column\_comment(table\_name, column\_name, comment)
49
+ * remove\_column\_comment(table\_name, column\_name, comment)
50
+ * set\_column\_comments(table\_name, comments)
51
+ * remove\_column\_comments(table\_name, *comments)
52
+
53
+
54
+ ### Examples
55
+
56
+ Set a comment on the given table.
57
+
58
+ set_table_comment :phone_numbers, 'This table stores phone numbers that conform to the North American Numbering Plan.'
59
+
60
+ Sets a comment on a given column of a given table.
61
+
62
+ set_column_comment :phone_numbers, :npa, 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.'
63
+
64
+ Removes any comment from the given table.
65
+
66
+ remove_table_comment :phone_numbers
67
+
68
+ Removes any comment from the given column of a given table.
69
+
70
+ remove_column_comment :phone_numbers, :npa
71
+
72
+ Set comments on multiple columns in the table.
73
+
74
+ set_column_comments :phone_numbers, :npa => 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.',
75
+ :nxx => 'Central Office Number'
76
+
77
+ Remove comments from multiple columns in the table.
78
+
79
+ remove_column_comments :phone_numbers, :npa, :nxx
80
+
81
+ PgPower also adds extra methods to change_table.
82
+
83
+ Set comments:
84
+
85
+ change_table :phone_numbers do |t|
86
+ t.set_table_comment 'This table stores phone numbers that conform to the North American Numbering Plan.'
87
+ t.set_column_comment :npa, 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.'
88
+ end
89
+
90
+ change_table :phone_numbers do |t|
91
+ t.set_column_comments :npa => 'Numbering Plan Area Code - Allowed ranges: [2-9] for first digit, [0-9] for second and third digit.',
92
+ :nxx => 'Central Office Number'
93
+ end
94
+
95
+ Remove comments:
96
+
97
+ change_table :phone_numbers do |t|
98
+ t.remove_table_comment
99
+ t.remove_column_comment :npa
100
+ end
101
+
102
+ change_table :phone_numbers do |t|
103
+ t.remove_column_comments :npa, :nxx
104
+ end
105
+
106
+ ## Foreign keys
107
+
108
+ We imported some code of [foreigner](https://github.com/matthuhiggins/foreigner)
109
+ gem and patched it to be schema-aware. We also added support for index auto-generation.
110
+
111
+ You should disable `foreigner` in your Gemfile if you want to use `pg_power`.
112
+
113
+ If you do not want to generate an index, pass the :exclude_index => true option.
114
+
115
+ The syntax is compatible with `foreigner`:
116
+
117
+
118
+ Add foreign key from `comments` to `posts` using `post_id` column as key by default:
119
+ add_foreign_key(:comments, :posts)
120
+
121
+ Specify key explicitly:
122
+ add_foreign_key(:comments, :posts, :column => :blog_post_id)
123
+
124
+ Specify name of foreign key constraint:
125
+ add_foreign_key(:comments, :posts, :name => "comments_posts_fk")
126
+
127
+ It works with schemas as expected:
128
+ add_foreign_key('blog.comments', 'blog.posts')
129
+
130
+ Adds the index 'index_comments_on_post_id':
131
+ add_foreign_key(:comments, :posts)
132
+
133
+ Does not add an index:
134
+ add_foreign_key(:comments, :posts, :exclude_index => true)
135
+
136
+ ## Partial Indexes
137
+
138
+ We used a Rails 4.x [pull request](https://github.com/rails/rails/pull/4956) as a
139
+ starting point, backported to Rails 3.1.x and patched it to be schema-aware.
140
+
141
+ ### Examples
142
+
143
+ Add a partial index to a table
144
+
145
+ add_index(:comments, [:country_id, :user_id], :where => 'active')
146
+
147
+ Add a partial index to a schema table
148
+
149
+ add_index('blog.comments', :user_id, :where => 'active')
150
+
151
+ ## Indexes on Expressions
152
+
153
+ PostgreSQL supports indexes on expressions. Right now, only basic functional
154
+ expressions are supported.
155
+
156
+ ### Examples
157
+
158
+ Add an index to a column with a function
159
+
160
+ add_index(:comments, "lower(text)")
161
+
162
+ ## Tools
163
+
164
+ PgPower::Tools provides number of useful methods:
165
+
166
+ PgPower::Tools.create_schema "services" # => create new PG schema "services"
167
+ PgPower::Tools.create_schema "nets" # => create new PG schema "nets"
168
+ PgPower::Tools.drop_schema "services" # => remove the PG schema "services"
169
+ PgPower::Tools.schemas # => ["public", "information_schema", "nets"]
170
+ PgPower::Tools.index_exists?(table, columns, options) # => returns true if an index exists for the given params
171
+
172
+ ## Running tests:
173
+
174
+ * Configure `spec/dummy/config/database.yml` for development and test environments.
175
+ * Run `rake spec`.
176
+ * Make sure migrations don't raise exceptions and all specs pass.
177
+
178
+ ## TODO:
179
+
180
+ Add next syntax to create table:
181
+
182
+ create_table "table_name", :schema => "schema_name" do |t|
183
+ # columns goes here
184
+ end
185
+
186
+ Support for JRuby:
187
+
188
+ * Jdbc driver provides its own `create_schema(schema, user)` method - solve conflicts.
189
+
190
+ ## Credits
191
+
192
+ * [Potapov Sergey](https://github.com/greyblake) - schema support
193
+ * [Arthur Shagall](https://github.com/albertosaurus) - thanks for [pg_comment](https://github.com/albertosaurus/pg_comment)
194
+ * [Matthew Higgins](https://github.com/matthuhiggins) - thanks for [foreigner](https://github.com/matthuhiggins/foreigner), which was used as a base for the foreign key support
195
+ * [Marcelo Silveira](https://github.com/mhfs) - thanks for rails partial index support that was backported into this gem
196
+
197
+ ## Copyright and License
198
+
199
+ Copyright (c) 2012 TMX Credit.
200
+ Initial foreign key code taken from foreigner, Copyright (c) 2009 Matthew Higgins
201
+ pg_comment Copyright (c) 2011 Arthur Shagall
202
+ Partial index Copyright (c) 2012 Marcelo Silveira
203
+
204
+ Released under the MIT License. See the MIT-LICENSE file for more details.
205
+
206
+ ## Contributing
207
+
208
+ Contributions are welcome. However, before issuing a pull request, please make sure of the following:
209
+
210
+ * All specs are passing (under both ree and 1.9.3)
211
+ * Any new features have test coverage.
212
+ * Anything that breaks backward compatibility has a very good reason for doing so.
@@ -0,0 +1,139 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters # :nodoc:
3
+ module SchemaStatements # :nodoc:
4
+ # Regexp used to find the function name and function argument of a
5
+ # function call
6
+ FUNCTIONAL_INDEX_REGEXP = /(\w+)\((\w+)\)/
7
+
8
+ # Adds a new index to the table. +column_name+ can be a single Symbol, or
9
+ # an Array of Symbols.
10
+ #
11
+ # ====== Creating a partial index
12
+ # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active")
13
+ # generates
14
+ # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
15
+ #
16
+ def add_index(table_name, column_name, options = {})
17
+ index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
18
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
19
+ end
20
+
21
+ # Checks to see if an index exists on a table for a given index definition.
22
+ #
23
+ # === Examples
24
+ # # Check that a partial index exists
25
+ # index_exists?(:suppliers, :company_id, :where => 'active')
26
+ #
27
+ # # GIVEN: "index_suppliers_on_company_id" UNIQUE, btree (company_id) WHERE active
28
+ # index_exists?(:suppliers, :company_id, :unique => true, :where => 'active') => true
29
+ # index_exists?(:suppliers, :company_id, :unique => true) => false
30
+ #
31
+ def index_exists?(table_name, column_name, options = {})
32
+ column_names = Array.wrap(column_name)
33
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
34
+
35
+ # Always compare the index name
36
+ default_comparator = lambda { |index| index.name == index_name }
37
+ comparators = [default_comparator]
38
+
39
+ # Add a comparator for each index option that is part of the query
40
+ index_options = [:unique, :where]
41
+ index_options.each do |index_option|
42
+ comparators << if options.key?(index_option)
43
+ lambda do |index|
44
+ pg_where_clause = index.send(index_option)
45
+ # pg does nothing to boolean clauses, e.g. 'where active' => 'where active'
46
+ if pg_where_clause.is_a?(TrueClass) or pg_where_clause.is_a?(FalseClass)
47
+ pg_where_clause == options[index_option]
48
+ else
49
+ # pg adds parentheses around non-boolean clauses, e.g. 'where color IS NULL' => 'where (color is NULL)'
50
+ pg_where_clause.gsub!(/[()]/,'')
51
+ # pg casts string comparison ::text. e.g. "where color = 'black'" => "where ((color)::text = 'black'::text)"
52
+ pg_where_clause.gsub!(/::text/,'')
53
+ # prevent case from impacting the comparison
54
+ pg_where_clause.downcase == options[index_option].downcase
55
+ end
56
+ end
57
+ else
58
+ # If the given index_option is not an argument to the index_exists? query,
59
+ # select only those pg indexes that do not have the component
60
+ lambda { |index| index.send(index_option).blank? }
61
+ end
62
+ end
63
+
64
+ # Search all indexes for any that match all comparators
65
+ indexes(table_name).any? do |index|
66
+ comparators.inject(true) { |ret, comparator| ret && comparator.call(index) }
67
+ end
68
+ end
69
+
70
+ # Derives the name of the index from the given table name and options hash.
71
+ def index_name(table_name, options) #:nodoc:
72
+ if Hash === options # legacy support
73
+ if options[:column]
74
+ column_names = Array.wrap(options[:column]).map {|c| expression_index_name(c)}
75
+ "index_#{table_name}_on_#{column_names * '_and_'}"
76
+ elsif options[:name]
77
+ options[:name]
78
+ else
79
+ raise ArgumentError, "You must specify the index name"
80
+ end
81
+ else
82
+ index_name(table_name, :column => options)
83
+ end
84
+ end
85
+
86
+ # Returns options used to build out index SQL
87
+ #
88
+ # Added support for partial indexes implemented using the :where option
89
+ #
90
+ def add_index_options(table_name, column_name, options = {})
91
+ column_names = Array(column_name)
92
+ index_name = index_name(table_name, :column => column_names)
93
+
94
+ if Hash === options # legacy support, since this param was a string
95
+ index_type = options[:unique] ? "UNIQUE" : ""
96
+ index_name = options[:name].to_s if options.key?(:name)
97
+ if supports_partial_index?
98
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
99
+ end
100
+ else
101
+ index_type = options
102
+ end
103
+
104
+ if index_name.length > index_name_length
105
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
106
+ end
107
+ if index_name_exists?(table_name, index_name, false)
108
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
109
+ end
110
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
111
+
112
+ [index_name, index_type, index_columns, index_options]
113
+ end
114
+ protected :add_index_options
115
+
116
+ # Override super method to provide support for expression column names
117
+ def quoted_columns_for_index(column_names, options = {})
118
+ column_names.map do |name|
119
+ if name =~ FUNCTIONAL_INDEX_REGEXP
120
+ "#{$1}(#{quote_column_name($2)})"
121
+ else
122
+ quote_column_name(name)
123
+ end
124
+ end
125
+ end
126
+ protected :quoted_columns_for_index
127
+
128
+ # Map an expression to a name appropriate for an index
129
+ def expression_index_name(column_name)
130
+ if column_name =~ FUNCTIONAL_INDEX_REGEXP
131
+ "#{$1.downcase}_#{$2}"
132
+ else
133
+ column_name
134
+ end
135
+ end
136
+ private :expression_index_name
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,135 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ # Patched version: 3.1.3
4
+ # Patched methods::
5
+ # * indexes
6
+ class PostgreSQLAdapter
7
+ # In Rails3.2 method #extract_schema_and_table is moved into Utils module.
8
+ # In Rails3.1 it's implemented right in PostgreSQLAdapter class.
9
+ # So it's Rails3.2 we include the module into PostgreSQLAdapter in order to make
10
+ # it compatible to Rails3.1
11
+ # -- sergey.potapov 2012-06-25
12
+ if ActiveRecord::VERSION::STRING =~ /^3\.2/
13
+ include self::Utils
14
+ end
15
+
16
+ # Regex to find columns used in index statements
17
+ INDEX_COLUMN_EXPRESSION = /ON \w+(?: USING \w+ )?\((.+)\)/
18
+ # Regex to find where clause in index statements
19
+ INDEX_WHERE_EXPRESION = /WHERE (.+)$/
20
+
21
+ # Returns an array of indexes for the given table.
22
+ #
23
+ # == Patch 1 reason:
24
+ # Since {ActiveRecord::SchemaDumper#tables} is patched to process tables
25
+ # with a schema prefix, the {#indexes} method receives table_name as
26
+ # "<schema>.<table>". This patch allows it to handle table names with
27
+ # a schema prefix.
28
+ #
29
+ # == Patch 1:
30
+ # Search using provided schema if table_name includes schema name.
31
+ #
32
+ # == Patch 2 reason:
33
+ # {ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#indexes} is patched
34
+ # to support partial indexes using :where clause.
35
+ #
36
+ # == Patch 2:
37
+ # Search the postgres indexdef for the where clause and pass the output to
38
+ # the custom {PgPower::ConnectionAdapters::IndexDefinition}
39
+ #
40
+ def indexes(table_name, name = nil)
41
+ schema, table = extract_schema_and_table(table_name)
42
+ schemas = schema ? "ARRAY['#{schema}']" : 'current_schemas(false)'
43
+
44
+ result = query(<<-SQL, name)
45
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
46
+ FROM pg_class t
47
+ INNER JOIN pg_index d ON t.oid = d.indrelid
48
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
49
+ WHERE i.relkind = 'i'
50
+ AND d.indisprimary = 'f'
51
+ AND t.relname = '#{table}'
52
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (#{schemas}) )
53
+ ORDER BY i.relname
54
+ SQL
55
+
56
+ result.map do |row|
57
+ index = {
58
+ :name => row[0],
59
+ :unique => row[1] == 't',
60
+ :keys => row[2].split(" "),
61
+ :definition => row[3],
62
+ :id => row[4]
63
+ }
64
+
65
+ column_names = find_column_names(table_name, index)
66
+
67
+ unless column_names.empty?
68
+ where = find_where_statement(index)
69
+ lengths = find_lengths(index)
70
+
71
+ PgPower::ConnectionAdapters::IndexDefinition.new(table_name, index[:name], index[:unique], column_names, lengths, where)
72
+ end
73
+ end.compact
74
+ end
75
+
76
+ # Find column names from index attributes. If the columns are virtual (ie
77
+ # this is an expression index) then it will try to return the functions
78
+ # that represent each column
79
+ #
80
+ # @param [String] table_name the name of the table
81
+ # @param [Hash] index index attributes
82
+ # @return [Array]
83
+ def find_column_names(table_name, index)
84
+ columns = Hash[query(<<-SQL, "Columns for index #{index[:name]} on #{table_name}")]
85
+ SELECT a.attnum, a.attname
86
+ FROM pg_attribute a
87
+ WHERE a.attrelid = #{index[:id]}
88
+ AND a.attnum IN (#{index[:keys].join(",")})
89
+ SQL
90
+
91
+ column_names = columns.values_at(*index[:keys]).compact
92
+
93
+ if column_names.empty?
94
+ definition = index[:definition].sub(INDEX_WHERE_EXPRESION, '')
95
+ if column_expression = definition.match(INDEX_COLUMN_EXPRESSION)[1]
96
+ column_names = column_expression.split(',').map do |functional_name|
97
+ remove_type(functional_name)
98
+ end
99
+ end
100
+ end
101
+
102
+ column_names
103
+ end
104
+
105
+ # Find where statement from index definition
106
+ #
107
+ # @param [Hash] index index attributes
108
+ # @return [String] where statement
109
+ def find_where_statement(index)
110
+ index[:definition].scan(INDEX_WHERE_EXPRESION).flatten[0]
111
+ end
112
+
113
+ # Find length of index
114
+ # TODO Update lengths once we merge in ActiveRecord code that supports it. -dresselm 20120305
115
+ #
116
+ # @param [Hash] index index attributes
117
+ # @return [Array]
118
+ def find_lengths(index)
119
+ []
120
+ end
121
+
122
+ # Remove type specification from stored Postgres index definitions
123
+ #
124
+ # @param [String] column_with_type the name of the column with type
125
+ # @return [String]
126
+ #
127
+ # @example
128
+ # remove_type("((col)::text")
129
+ # => "col"
130
+ def remove_type(column_with_type)
131
+ column_with_type.sub(/\((\w+)\)::\w+/, '\1')
132
+ end
133
+ end
134
+ end
135
+ end