Skip to content

Commit

Permalink
Merge pull request #17 from kbrock/more_postgres_features
Browse files Browse the repository at this point in the history
More postgres features
  • Loading branch information
danmcclain committed Oct 3, 2012
2 parents c0b4e54 + ff7ee60 commit 2a8908f
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 42 additions & 3 deletions lib/postgres_ext/active_record/schema_dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions postgres_ext.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
2 changes: 1 addition & 1 deletion spec/dummy/config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 22 additions & 0 deletions spec/migrations/index_spec.rb
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions spec/schema_dumper/index_spec.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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__), '../')
Expand Down

0 comments on commit 2a8908f

Please sign in to comment.