pg_power 1.0.0

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