module ODBCAdapter module Adapters # Overrides specific to PostgreSQL. Mostly taken from # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter class PostgreSQLODBCAdapter < ActiveRecord::ConnectionAdapters::ODBCAdapter class BindSubstitution < Arel::Visitors::PostgreSQL include Arel::Visitors::BindVisitor end PRIMARY_KEY = 'SERIAL PRIMARY KEY' # Filter for ODBCAdapter#tables # Omits table from #tables if table_filter returns true def table_filter(schema_name, table_type) %w[information_schema pg_catalog].include?(schema_name) || table_type !~ /TABLE/i end def truncate(table_name, name = nil) exec_query("TRUNCATE TABLE #{quote_table_name(table_name)}", name) end # Returns the sequence name for a table's primary key or some other specified key. def default_sequence_name(table_name, pk = nil) #:nodoc: serial_sequence(table_name, pk || 'id').split('.').last rescue ActiveRecord::StatementInvalid "#{table_name}_#{pk || 'id'}_seq" end # Returns the current ID of a table's sequence. def last_insert_id(sequence_name) r = exec_query("SELECT currval('#{sequence_name}')", 'SQL') Integer(r.rows.first.first) end # Executes an INSERT query and returns the new record's ID def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) unless pk table_ref = extract_table_ref_from_insert_sql(sql) pk = primary_key(table_ref) if table_ref end if pk select_value("#{sql} RETURNING #{quote_column_name(pk)}") else super end end alias :create :insert def sql_for_insert(sql, pk, id_value, sequence_name, binds) unless pk table_ref = extract_table_ref_from_insert_sql(sql) pk = primary_key(table_ref) if table_ref end sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk [sql, binds] end def type_cast(value, column) return super unless column case value when String return super unless 'bytea' == column.sql_type { value: value, format: 1 } else super end end # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(string) string.gsub(/\\/, '\&\&').gsub(/'/, "''") end def quoted_true "'t'" end def quoted_false "'f'" end def disable_referential_integrity #:nodoc: execute(tables.map { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(';')) yield ensure execute(tables.map { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(';')) end # Create a new PostgreSQL database. Options include :owner, :template, # :encoding, :tablespace, and :connection_limit (note that MySQL # uses :charset while PostgreSQL uses :encoding). # # Example: # create_database config[:database], config # create_database 'foo_development', :encoding => 'unicode' def create_database(name, options = {}) options = options.reverse_merge(encoding: 'utf8') option_string = options.symbolize_keys.sum do |key, value| case key when :owner " OWNER = \"#{value}\"" when :template " TEMPLATE = \"#{value}\"" when :encoding " ENCODING = '#{value}'" when :tablespace " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" else "" end end execute("CREATE DATABASE #{quote_table_name(name)}#{option_string}") end # Drops a PostgreSQL database. # # Example: # drop_database 'matt_development' def drop_database(name) #:nodoc: execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end # Renames a table. def rename_table(name, new_name) execute("ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}") end def change_column(table_name, column_name, type, options = {}) execute("ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) end def change_column_default(table_name, column_name, default) execute("ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{quote(default)}") end def rename_column(table_name, column_name, new_column_name) execute("ALTER TABLE #{table_name} RENAME #{column_name} TO #{new_column_name}") end def remove_index!(_table_name, index_name) execute("DROP INDEX #{quote_table_name(index_name)}") end def rename_index(table_name, old_name, new_name) execute("ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}") end # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. # # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # requires that the ORDER BY include the distinct column. # # distinct("posts.id", "posts.created_at desc") def distinct(columns, orders) return "DISTINCT #{columns}" if orders.empty? # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers order_columns = orders.map { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') } order_columns.reject! { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } "DISTINCT #{columns}, #{order_columns * ', '}" end private def serial_sequence(table, column) result = exec_query(<<-eosql, 'SCHEMA') SELECT pg_get_serial_sequence('#{table}', '#{column}') eosql result.rows.first.first end end end end