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:


69
70
71
72
73
74
75
76
77
78
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 69

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 PostgreSQL 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

  • :operator_class - an operator class name or a hash mapping column name to operator class name

  • +: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.


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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 95

def add_index(table_name, column_name, options = {})
  options = {} if options.nil?  # some callers explicitly pass options=nil
  column_name, options = [], column_name if column_name.is_a?(Hash)
  column_names = Array(column_name).compact
  column_names += Array(options[:with] || [])
  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" : nil
  index_name = options[:name] || index_name(table_name, column_names)
  concurrently = options[:algorithm] == :concurrently
  conditions = options[:conditions]
  kind       = options[:kind]
  operator_classes = options[:operator_class]
  if operator_classes and not operator_classes.is_a? Hash
    operator_classes = Hash[column_names.map {|name| [name, operator_classes]}]
  end

  sql = []
  sql << 'CREATE'
  sql << index_type
  sql << 'INDEX'
  sql << 'CONCURRENTLY' if concurrently
  sql << quote_column_name(index_name)
  sql << 'ON'
  sql << quote_table_name(table_name)

  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 << expression
  else
    option_strings = Hash[column_names.map {|name| [name, '']}]
    (operator_classes||{}).each do |column, opclass|
      option_strings[column] += " #{opclass}" if opclass
    end
    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 << expression
    sql << "WHERE (#{ ::ActiveRecord::Base.send(:sanitize_sql, conditions, quote_table_name(table_name)) })" if conditions
  end
  execute sql.compact.join(' ')
rescue => e
  SchemaStatements.add_index_exception_handler(self, table_name, column_names, options, e)
end

#alter_enum(name, value, options = {}) ⇒ Object


360
361
362
363
364
365
366
367
368
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 360

def alter_enum(name, value, options = {})
  opts = case
         when options[:before] then "BEFORE #{escape_enum_value(options[:before])}"
         when options[:after] then "AFTER #{escape_enum_value(options[:after])}"
         else
           ''
         end
  execute "ALTER TYPE #{enum_name(name, options[:schema])} ADD VALUE #{escape_enum_value(value)} #{opts}"
end

#create_enum(name, *values) ⇒ Object


354
355
356
357
358
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 354

def create_enum(name, *values)
  options = values.extract_options!
  list = values.map { |value| escape_enum_value(value) }
  execute "CREATE TYPE #{enum_name(name, options[:schema])} AS ENUM (#{list.join(',')})"
end

#drop_enum(name, options = {}) ⇒ Object


370
371
372
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 370

def drop_enum(name, options = {})
  execute "DROP TYPE #{enum_name(name, options[:schema])}"
end

#drop_table(name, options = {}) ⇒ Object

pg gem defines a drop_table with fewer options than our Abstract one, so use the abstract one instead


376
377
378
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 376

def drop_table(name, options={})
  SchemaPlus::ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_method(:drop_table).bind(self).call(name, options)
end

#enumsObject

:nodoc:


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 329

def enums #:nodoc:
  result = query(<<-SQL)
    SELECT
      N.nspname AS schema_name,
      T.typname AS enum_name,
      E.enumlabel AS enum_label,
      E.enumsortorder AS enum_sort_order
      --array_agg(E.enumlabel ORDER BY enumsortorder) AS labels
    FROM pg_type T
    JOIN pg_enum E ON E.enumtypid = T.oid
    JOIN pg_namespace N ON N.oid = T.typnamespace
    ORDER BY 1, 2, 4
  SQL

  result.reduce([]) do |res, row|
    last = res.last
    if last && last[0] == row[0] && last[1] == row[1]
      last[2] << row[2]
    else
      res << (row[0..1] << [row[2]])
    end
    res
  end
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)


263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 263

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:


284
285
286
287
288
289
290
291
292
293
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 284

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.)


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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 167

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,
          d.indclass
   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, indclass)|
    unique = (is_unique == 't' || is_unique == true) # The test against true is for JDBC which is returning a boolean and not a String.
    index_keys = indkey.split(" ")
    opclasses = indclass.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

    opclass_name  = {}
    rows = query(<<-SQL, "Op classes for index #{index_name} on #{table_name}")
      SELECT oid, opcname FROM pg_opclass
      WHERE (NOT opcdefault) AND oid IN (#{opclasses.join(',')})
    SQL
    rows.each do |oid, opcname|
      opclass_name[oid.to_s] = opcname
    end
    operator_classes = {}
    column_names.zip(opclasses).each do |column_name, opclass|
      operator_classes[column_name] = opclass_name[opclass]
    end
    operator_classes.delete_if{|k,v| v.nil?}

    # 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,
                                                            :operator_classes => operator_classes,
                                                            :expression => expression)
  end
end

#query(*args) ⇒ Object


251
252
253
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 251

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

#rename_table_with_schema_plus(oldname, newname) ⇒ Object

:nodoc:


255
256
257
258
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 255

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:


295
296
297
298
299
300
301
302
303
304
305
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 295

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:


158
159
160
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 158

def supports_partial_indexes? #:nodoc:
  true
end

#view_definition(view_name, name = nil) ⇒ Object

:nodoc:


318
319
320
321
322
323
324
325
326
327
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 318

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:


307
308
309
310
311
312
313
314
315
316
# File 'lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb', line 307

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