0

I'm trying to write (Ruby) script that will drop all the foreign key and unique constraints in my PostgreSQL DB and then re-add them.

The FK part seems to be working OK.

However, dropping and recreating unique constraints isn't working.

I think the reason is that when the unique constraint is created, PostgreSQL creates an index along with it, and that index doesn't get automatically dropped when the unique constraint is dropped. So then when the script tries to re-add the unique constraint, I get an error like...

PG::Error: ERROR:  relation "unique_username" already exists
: ALTER TABLE users ADD CONSTRAINT unique_username UNIQUE (username)

And indeed when I look at the DB in the pgAdmin GUI utility, that index exists.

The question is, how do I find it in my script and drop it?


Here's my script...

manage_constraints.rake

namespace :journal_app do

  desc 'Drop constraints'
  task :constraints_drop => :environment do

    sql = %Q|
SELECT
  constraint_name, table_catalog, table_name
FROM
  information_schema.table_constraints
WHERE
  table_catalog = 'journal_app_#{Rails.env}'
AND
  constraint_name NOT LIKE '%_pkey'
AND
  constraint_name NOT LIKE '%_not_null';
|

    results = execute_sql(sql)

    results.each do |row|
      puts "Dropping constraint #{row['constraint_name']} from table #{row['table_name']}."
      execute_sql("ALTER TABLE #{row['table_name']} DROP CONSTRAINT #{row['constraint_name']}")
    end

  end

  # --------------------------------------------------------------------------------------------------------------------

  desc 'Drops constraints, then adds them'
  task :constraints_add => :environment do

    Rake::Task['journal_app:constraints_drop'].invoke

    UNIQUE_KEYS = [
        {
            :name => 'unique_username',
            :table => 'users',
            :columns => ['username']
        },
        {
            :name => 'unique_email',
            :table => 'users',
            :columns => ['email']
        }
    ]

    FKs = [
        {
            :name => 'fk_entries_users',
            :parent_table => 'users',
            :child_table => 'entries',
            :on_delete => 'CASCADE'
        },
        {
            :name => 'fk_entries_entry_tags',
            :parent_table => 'entries',
            :child_table => 'entry_tags',
            :on_delete => 'CASCADE'
        },

        # etc...

    ]

    UNIQUE_KEYS.each do |constraint|
      sql = "ALTER TABLE #{constraint[:table]} ADD CONSTRAINT #{constraint[:name]} UNIQUE (#{constraint[:columns].join(', ')})"
      puts "Adding unique constraint #{constraint[:name]} to table #{constraint[:table]}."
      puts '  SQL:'
      puts "    #{sql}"
      execute_sql(sql)
    end

    FKs.each do |fk|
      sql = %Q|
ALTER TABLE #{fk[:child_table]} ADD CONSTRAINT #{fk[:name]} FOREIGN KEY (#{fk[:parent_table].singularize}_id)
  REFERENCES #{fk[:parent_table]} (id)
    ON UPDATE NO ACTION ON DELETE #{fk[:on_delete]}|.strip!
      puts "Adding foreign key #{fk[:name]}."
      puts '  SQL:'
      puts "    #{sql}"
      execute_sql(sql)
    end

  end

end

def execute_sql(sql)
  ActiveRecord::Base.connection.execute(sql)
end
Ethan
  • 57,819
  • 63
  • 187
  • 237
  • There's no distinction between a unique constraint and a unique index. Why don't you start executing your queries on the psql command line first instead of adding a layer of potential obfuscation. Also, note that constraints and index names are not local to a table, that is why they usually begin with the table name when you let the db choose a default name for you. – Dondi Michael Stroma Sep 26 '12 at 04:45
  • "*that index doesn't get automatically dropped when the unique constraint is dropped*". That is wrong. The index ***is*** dropped when you drop the unique constraint. Did you maybe not `commit` the drop and therefor pgAdmin is still showing it? –  Sep 26 '12 at 12:49

1 Answers1

1

First, why do such a thing? This has the feel of one of those "I've decided on solution Y to problem X, and am having a problem with solution Y that I'm asking about" - where the real answer is "use solution Z not solution Y to solve problem X". In other words, try explaining the underlying problem you are having, there might be a much better way to solve it.

If you must do it, query pg_catalog.pg_index inner join pg_class on pg_class.oid = pg_index.indexrelid for indexes that are not indisprimary and exclude anything with EXISTS (SELECT 1 FROM pg_constraint on pg_index.indrelid = pg_constraint.conindid).

eg:

SELECT pg_class.relname
FROM pg_index INNER JOIN pg_class ON (pg_class.oid = pg_index.indexrelid) 
INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid) 
WHERE NOT EXISTS (
    SELECT 1 FROM pg_constraint WHERE pg_index.indrelid = pg_constraint.conindid
) 
AND pg_index.indisprimary = 'f'
AND pg_namespace.nspname NOT LIKE 'pg_%';

Be aware that such queries may break in any major version transition as the pg_catalog is not guaranteed to retain the same schema across versions. Query the Pg version and use version-specific queries if necessary. Sound painful? It is, but it shouldn't generally be necessary, you're just doing something kind of weird.

For most purposes the very stable information_schema is quite sufficient.

Craig Ringer
  • 307,061
  • 76
  • 688
  • 778
  • Thanks for the answer. Yes, that does sound painful. Why do such a thing? The reason is that I want to write a script to maintain the constraints instead of applying them one-by-one using psql or some other client. That way I can have all my constraints listed in one file, I can keep them consistent across development and production DBs, and I can keep the script in version control. – Ethan Sep 26 '12 at 18:44
  • @Ethan Check out database DDL versioning technologies like [Post Facto](https://www.pgcon.org/2009/schedule/events/158.en.html), and systems that let you keep the whole DB definition including tables, constraints, etc in a versioned descriptor. Doesn't Rails do this for you with migrations? Or does rails not understand constraints? – Craig Ringer Sep 26 '12 at 23:29
  • Thanks for the info. Yeah, I don't think Rails migrations support FK constraints. – Ethan Sep 28 '12 at 02:51
  • @Ethan Looks like it doesn't directly, but you can still do it in migrations with some hand-written SQL. http://stackoverflow.com/questions/4664520/how-do-i-add-a-check-constraint-in-a-rails-migration – Craig Ringer Sep 28 '12 at 09:11