Module: SchemaPlus::ActiveRecord::ConnectionAdapters::PostgresqlAdapter

Defined in:
lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb

Overview

The Postgresql adapter implements the SchemaPlus extensions and enhancements

Defined Under Namespace

Modules: AddColumnOptions

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object

:nodoc:



60
61
62
63
64
65
66
67
68
69
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 60

def self.included(base) #:nodoc:
  base.class_eval do
    if ::ActiveRecord::VERSION::MAJOR.to_i < 4 && !defined?(JRUBY_VERSION)
      remove_method :indexes
    end
    alias_method_chain :rename_table, :schema_plus
    alias_method_chain :exec_cache, :schema_plus unless defined?(JRUBY_VERSION)
  end
  ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.send(:include, PostgreSQLColumn) unless ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.include?(PostgreSQLColumn)
end

Instance Method Details

#add_index(table_name, column_name, options = {}) ⇒ Object

SchemaPlus provides the following extra options for Postgres indexes:

  • :conditions - SQL conditions for the WHERE clause of the index

  • :expression - SQL expression to index. column_name can be nil or ommitted, in which case :name must be provided

  • :kind - index method for Postgresql to use

  • +:case_sensitive - setting to false is a shorthand for :expression => ‘LOWER(column_name)’

The :case_sensitive => false option ties in with Rails built-in support for case-insensitive searching:

validates_uniqueness_of :name, :case_sensitive => false

Since since :case_sensitive => false is implemented by using :expression, this raises an ArgumentError if both are specified simultaneously.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 85

def add_index(table_name, column_name, options = {})
  column_name, options = [], column_name if column_name.is_a?(Hash)
  column_names = Array(column_name).compact
  if column_names.empty?
    raise ArgumentError, "No columns and :expression missing from options - cannot create index" unless options[:expression]
    raise ArgumentError, "Index name not given. Pass :name option" unless options[:name]
  end

  index_type = options[:unique] ? "UNIQUE" : ""
  index_name = options[:name] || index_name(table_name, column_names)
  conditions = options[:conditions]
  kind       = options[:kind]

  if expression = options[:expression] then
    raise ArgumentError, "Cannot specify :case_sensitive => false with an expression.  Use LOWER(column_name)" if options[:case_sensitive] == false
    # Wrap expression in parentheses if necessary
    expression = "(#{expression})" if expression !~ /(using|with|tablespace|where)/i
    expression = "USING #{kind} #{expression}" if kind
    expression = "#{expression} WHERE #{conditions}" if conditions

    sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{expression}"
  else
    option_strings = Hash[column_names.map {|name| [name, '']}]
    option_strings = add_index_sort_order(option_strings, column_names, options)

    if options[:case_sensitive] == false
      caseable_columns = columns(table_name).select { |col| [:string, :text].include?(col.type) }.map(&:name)
      quoted_column_names = column_names.map do |col_name|
        (caseable_columns.include?(col_name.to_s) ? "LOWER(#{quote_column_name(col_name)})" : quote_column_name(col_name)) + option_strings[col_name]
      end
    else
      quoted_column_names = column_names.map { |col_name| quote_column_name(col_name) + option_strings[col_name] }
    end

    expression = "(#{quoted_column_names.join(', ')})"
    expression = "USING #{kind} #{expression}" if kind

    sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{expression}"
    sql += " WHERE (#{ ::ActiveRecord::Base.send(:sanitize_sql, conditions, quote_table_name(table_name)) })" if conditions
  end
  execute sql
rescue => e
  SchemaStatements.add_index_exception_handler(self, table_name, column_names, options, e)
end

#exec_cache_with_schema_plus(sql, *args) ⇒ Object

Prepass to replace each ActiveRecord::DB_DEFAULT with a literal DEFAULT in the sql string. (The underlying pg gem provides no way to bind a value that will replace $n with DEFAULT)



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 218

def exec_cache_with_schema_plus(sql, *args)
  name_passed = (2 == args.size)
  binds, name = args.reverse

  if binds.any?{ |col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
    j = 0
    binds.each_with_index do |(col, val), i|
    if val.equal? ::ActiveRecord::DB_DEFAULT
      sql = sql.sub(/\$#{i+1}/, 'DEFAULT')
    else
      sql = sql.sub(/\$#{i+1}/, "$#{j+1}") if i != j
      j += 1
    end
    end
    binds = binds.reject{|col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
  end

  args = name_passed ? [name, binds] : [binds]
  exec_cache_without_schema_plus(sql, *args)
end

#foreign_keys(table_name, name = nil) ⇒ Object

:nodoc:



239
240
241
242
243
244
245
246
247
248
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 239

def foreign_keys(table_name, name = nil) #:nodoc:
  load_foreign_keys(<<-SQL, name)
SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
  FROM pg_class t, pg_constraint f
 WHERE f.conrelid = t.oid
   AND f.contype = 'f'
   AND t.relname = '#{table_name_without_namespace(table_name)}'
   AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
  SQL
end

#indexes(table_name, name = nil) ⇒ Object

This method entirely duplicated from AR’s postgresql_adapter.c, but includes the extra bit to determine the column name for a case-insensitive index. (Haven’t come up with any clever way to only code up the case-insensitive column name bit here and otherwise use the existing method.)



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 139

def indexes(table_name, name = nil) #:nodoc:
  result = query(<<-SQL, name)

   SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
          m.amname, pg_get_expr(d.indpred, t.oid) as conditions, pg_get_expr(d.indexprs, t.oid) as expression
   FROM pg_class t
   INNER JOIN pg_index d ON t.oid = d.indrelid
   INNER JOIN pg_class i ON d.indexrelid = i.oid
   INNER JOIN pg_am m ON i.relam = m.oid
   WHERE i.relkind = 'i'
     AND d.indisprimary = 'f'
     AND t.relname = '#{table_name_without_namespace(table_name)}'
     AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
  ORDER BY i.relname
  SQL

  result.map do |(index_name, is_unique, indkey, inddef, oid, kind, conditions, expression)|
    unique = (is_unique == 't')
    index_keys = indkey.split(" ")

    rows = query(<<-SQL, "Columns for index #{index_name} on #{table_name}")
      SELECT CAST(a.attnum as VARCHAR), a.attname, t.typname
      FROM pg_attribute a
      INNER JOIN pg_type t ON a.atttypid = t.oid
      WHERE a.attrelid = #{oid}
    SQL
    columns = {}
    types = {}
    rows.each do |num, name, type|
      columns[num] = name
      types[name] = type
    end

    column_names = columns.values_at(*index_keys).compact
    case_sensitive = true

    # extract column names from the expression, for a
    # case-insensitive index.
    # only applies to character, character varying, and text
    if expression
      rexp_lower = %r{\blower\(\(?([^)]+)(\)::text)?\)}
      if expression.match /\A#{rexp_lower}(?:, #{rexp_lower})*\z/
        case_insensitive_columns = expression.scan(rexp_lower).map(&:first).select{|column| %W[char varchar text].include? types[column]}
        if case_insensitive_columns.any?
          case_sensitive = false
          column_names = index_keys.map { |index_key|
            index_key == '0' ? case_insensitive_columns.shift : columns[index_key]
          }.compact
        end
      end
    end

    # 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[column_names.map {|column| [column, desc_order_columns.include?(column) ? :desc : :asc]}] : {}

    ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, column_names,
                                                            :name => index_name,
                                                            :unique => unique,
                                                            :orders => orders,
                                                            :conditions => conditions,
                                                            :case_sensitive => case_sensitive,
                                                            :kind => kind.downcase == "btree" ? nil : kind,
                                                            :expression => expression)
  end
end

#query(*args) ⇒ Object



206
207
208
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 206

def query(*args)
  select(*args).map(&:values)
end

#rename_table_with_schema_plus(oldname, newname) ⇒ Object

:nodoc:



210
211
212
213
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 210

def rename_table_with_schema_plus(oldname, newname) #:nodoc:
  rename_table_without_schema_plus(oldname, newname)
  rename_indexes_and_foreign_keys(oldname, newname)
end

#reverse_foreign_keys(table_name, name = nil) ⇒ Object

:nodoc:



250
251
252
253
254
255
256
257
258
259
260
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 250

def reverse_foreign_keys(table_name, name = nil) #:nodoc:
  load_foreign_keys(<<-SQL, name)
SELECT f.conname, pg_get_constraintdef(f.oid), t2.relname
  FROM pg_class t, pg_class t2, pg_constraint f
 WHERE f.confrelid = t.oid
   AND f.conrelid = t2.oid
   AND f.contype = 'f'
   AND t.relname = '#{table_name_without_namespace(table_name)}'
   AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{namespace_sql(table_name)} )
  SQL
end

#supports_partial_indexes?Boolean

:nodoc:

Returns:

  • (Boolean)


130
131
132
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 130

def supports_partial_indexes? #:nodoc:
  true
end

#view_definition(view_name, name = nil) ⇒ Object

:nodoc:



272
273
274
275
276
277
278
279
280
281
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 272

def view_definition(view_name, name = nil) #:nodoc:
  result = query(<<-SQL, name)
SELECT pg_get_viewdef(oid)
  FROM pg_class
 WHERE relkind = 'v'
   AND relname = '#{view_name}'
  SQL
  row = result.first
  row.first.chomp(';') unless row.nil?
end

#views(name = nil) ⇒ Object

:nodoc:



262
263
264
265
266
267
268
269
270
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 262

def views(name = nil) #:nodoc:
  sql = <<-SQL
    SELECT viewname
      FROM pg_views
    WHERE schemaname = ANY (current_schemas(false))
  SQL
  sql += " AND schemaname != 'postgis'" if adapter_name == 'PostGIS'
  query(sql, name).map { |row| row[0] }
end