module ODBCAdapter module Adapters # Overrides specific to PostgreSQL. Mostly taken from # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter class PostgreSQLODBCAdapter < ActiveRecord::ConnectionAdapters::ODBCAdapter BOOLEAN_TYPE = 'bool'.freeze PRIMARY_KEY = 'SERIAL PRIMARY KEY'.freeze alias :create :insert # Override to handle booleans appropriately def native_database_types @native_database_types ||= super.merge(boolean: { name: 'bool' }) end def arel_visitor Arel::Visitors::PostgreSQL.new(self) end # 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) serial_sequence(table_name, pk || 'id').split('.').last rescue ActiveRecord::StatementInvalid "#{table_name}_#{pk || 'id'}_seq" end 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.native_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 disable_referential_integrity 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 'rails_development' def drop_database(name) 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 protected # 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 # 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 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