From ff7ee605cdcd5479f377b5908bc3c484e04a938b Mon Sep 17 00:00:00 2001 From: Keenan Brock Date: Tue, 25 Sep 2012 16:54:42 -0400 Subject: [PATCH] add ability to specify index_type and where in creating / dumping indexes. add extension point for enhancing column_specs --- CHANGELOG.md | 4 ++ README.md | 19 ++++++ .../connection_adapters/postgres_adapter.rb | 60 +++++++++++++++++++ .../active_record/schema_dumper.rb | 45 +++++++++++++- postgres_ext.gemspec | 9 ++- spec/dummy/config/environments/development.rb | 2 +- spec/migrations/index_spec.rb | 22 +++++++ spec/schema_dumper/index_spec.rb | 21 +++++++ spec/spec_helper.rb | 1 + 9 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 spec/migrations/index_spec.rb create mode 100644 spec/schema_dumper/index_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc553f..998c97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.9 + +Adds more robust index types with add_index options :index_type and :where. + ## 0.0.8 Fixes add and change column diff --git a/README.md b/README.md index 634e4e8..f9005ae 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,25 @@ user_arel = User.arel_table User.where(user_arel[:ip_address].contained_witin('127.0.0.1/24')).to_sql # => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip_address\" << '127.0.0.1/24' ``` + +## Indexes + +### Index types + +Postgres\_ext allows you to specify an index type at index creation. + +```ruby +add_index :table_name, :column, :index_type => :gin +``` + +### Where clauses + +Postgres\_ext allows you to specify a where clause at index creation. + +```ruby +add_index :table_name, :column, :where => 'column < 50' +``` + ## Authors Dan McClain [twitter](http://twitter.com/_danmcclain) [github](http://github.com/danmcclain) diff --git a/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb b/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb index 066c3d6..d53618e 100644 --- a/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb +++ b/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb @@ -4,6 +4,11 @@ module ActiveRecord module ConnectionAdapters + # class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :index_type) + class IndexDefinition + attr_accessor :index_type, :where + end + class PostgreSQLColumn include PgArrayParser attr_accessor :array @@ -179,6 +184,15 @@ def add_column_options!(sql, options) super end + def add_index(table_name, column_name, options = {}) + index_name, unique, index_columns, _ = add_index_options(table_name, column_name, options) + if options.is_a? Hash + index_type = options[:index_type] ? " USING #{options[:index_type]} " : "" + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end + execute "CREATE #{unique} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}#{index_type}(#{index_columns})#{index_options}" + end + def change_table(table_name, options = {}) if supports_bulk_alter? && options[:bulk] recorder = ActiveRecord::Migration::CommandRecorder.new(self) @@ -242,6 +256,52 @@ def quote_with_extended_types(value, column = nil) end alias_method_chain :quote, :extended_types + # this is based upon rails 4 changes to include different index methods + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + result = select_rows(<<-SQL, name) + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + ORDER BY i.relname + SQL + result.map do |row| + index_name = row[0] + unique = row[1] == 't' + indkey = row[2].split(" ") + inddef = row[3] + oid = row[4] + + columns = Hash[select_rows(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] + SELECT a.attnum::text, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL + column_names = columns.values_at(*indkey).compact + + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + #changed from rails 3.2 + where = inddef.scan(/WHERE (.+)$/).flatten[0] + index_type = inddef.scan(/USING (.+?) /).flatten[0].to_sym + if column_names.present? + index_def = IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) + index_def.where = where + index_def.index_type = index_type if index_type && index_type != :btree + index_def + # else nil + end + #/changed + end.compact + end + private def ipaddr_to_string(value) diff --git a/lib/postgres_ext/active_record/schema_dumper.rb b/lib/postgres_ext/active_record/schema_dumper.rb index 61807e3..896d312 100644 --- a/lib/postgres_ext/active_record/schema_dumper.rb +++ b/lib/postgres_ext/active_record/schema_dumper.rb @@ -2,6 +2,11 @@ module ActiveRecord class SchemaDumper + VALID_COLUMN_SPEC_KEYS = [:name, :limit, :precision, :scale, :default, :null, :array] + def self.valid_column_spec_keys + VALID_COLUMN_SPEC_KEYS + end + private def table(table, stream) columns = @connection.columns(table) @@ -30,11 +35,13 @@ def table(table, stream) column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? next if column.name == pk - column_spec(column) + spec = column_spec(column) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} + spec end.compact # find all migration keys used in this table - keys = [:name, :limit, :precision, :scale, :default, :null, :array] & column_specs.map{ |k| k.keys }.flatten + keys = self.class.valid_column_spec_keys & column_specs.map{ |k| k.keys }.flatten # figure out the lengths for each column based on above keys lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } @@ -73,6 +80,37 @@ def table(table, stream) stream end + #mostly rails 3.2 code + def indexes(table, stream) + if (indexes = @connection.indexes(table)).any? + add_index_statements = indexes.map do |index| + statement_parts = [ + ('add_index ' + index.table.inspect), + index.columns.inspect, + (':name => ' + index.name.inspect), + ] + statement_parts << ':unique => true' if index.unique + + index_lengths = (index.lengths || []).compact + statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? + + index_orders = (index.orders || {}) + statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + + # changed from rails 2.3 + statement_parts << (':where => ' + index.where.inspect) if index.where + statement_parts << (':index_type => ' + index.index_type.inspect) if index.index_type + # /changed + + ' ' + statement_parts.join(', ') + end + + stream.puts add_index_statements.sort.join("\n") + stream.puts + end + end + + #mostly rails 3.2 code (pulled out of table method) def column_spec(column) spec = {} spec[:name] = column.name.inspect @@ -89,8 +127,9 @@ def column_spec(column) spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null spec[:default] = default_string(column.default) if column.has_default? + # changed from rails 3.2 code spec[:array] = 'true' if column.respond_to?(:array) && column.array - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} + # /changed spec end end diff --git a/postgres_ext.gemspec b/postgres_ext.gemspec index 4d97a65..12f1cc6 100644 --- a/postgres_ext.gemspec +++ b/postgres_ext.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |gem| gem.version = PostgresExt::VERSION gem.add_dependency 'activerecord', '~> 3.2.0' - gem.add_dependency 'pg_array_parser', '~> 0.0.3' + gem.add_dependency 'pg_array_parser', '~> 0.0.8' gem.add_development_dependency 'rails', '~> 3.2.0' gem.add_development_dependency 'rspec-rails', '~> 2.9.0' @@ -27,8 +27,11 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'pg', '~> 0.13.2' end unless ENV['CI'] - gem.add_development_dependency 'debugger', '~> 1.1.2' if RUBY_VERSION == '1.9.3' - gem.add_development_dependency 'ruby-debug' if RUBY_PLATFORM =~ /java/ + if RUBY_PLATFORM =~ /java/ + gem.add_development_dependency 'ruby-debug' + elsif RUBY_VERSION == '1.9.3' + gem.add_development_dependency 'debugger', '~> 1.1.2' + end end gem.add_development_dependency 'fivemat' end diff --git a/spec/dummy/config/environments/development.rb b/spec/dummy/config/environments/development.rb index 414aec4..8bfd558 100644 --- a/spec/dummy/config/environments/development.rb +++ b/spec/dummy/config/environments/development.rb @@ -1,5 +1,5 @@ Dummy::Application.configure do - require 'debugger' if RUBY_VERSION == '1.9.3' + require 'debugger' if RUBY_VERSION == '1.9.3' && ! ENV['CI'] # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on diff --git a/spec/migrations/index_spec.rb b/spec/migrations/index_spec.rb new file mode 100644 index 0000000..25798a8 --- /dev/null +++ b/spec/migrations/index_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe 'index migrations' do + let!(:connection) { ActiveRecord::Base.connection } + it 'creates special index' do + lambda do + connection.create_table :index_types do |t| + t.integer :col1, :array => true + t.integer :col2 + end + connection.add_index(:index_types, :col1, :index_type => :gin) + connection.add_index(:index_types, :col2, :where => '(col2 > 50)') + end.should_not raise_exception + + indexes = connection.indexes(:index_types) + index_1 = indexes.detect { |c| c.columns.map(&:to_s) == ['col1']} + index_2 = indexes.detect { |c| c.columns.map(&:to_s) == ['col2']} + + index_1.index_type.to_s.should eq 'gin' + index_2.where.should match /col2 > 50/ + end +end diff --git a/spec/schema_dumper/index_spec.rb b/spec/schema_dumper/index_spec.rb new file mode 100644 index 0000000..b115831 --- /dev/null +++ b/spec/schema_dumper/index_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'index schema dumper' do + let!(:connection) { ActiveRecord::Base.connection } + it 'correctly generates index statements' do + connection.create_table :index_types do |t| + t.integer :col1, :array => true + t.integer :col2 + end + connection.add_index(:index_types, :col1, :index_type => :gin) + connection.add_index(:index_types, :col2, :where => '(col2 > 50)') + + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(connection, stream) + output = stream.string + + output.should match /:index_type => :gin/ + output.should_not match /:index_type => :btree/ + output.should match /:where => "\(col2 > 50\)"/ + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d23a408..f21441b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ require File.expand_path('../dummy/config/environment.rb', __FILE__) require 'rspec/rails' +require 'rspec/autorun' require 'bourne' ENGINE_RAILS_ROOT=File.join(File.dirname(__FILE__), '../')