Class: ActiveRecord::ConnectionAdapters::FbAdapter

Inherits:
AbstractAdapter
  • Object
show all
Defined in:
lib/active_record/connection_adapters/fb_adapter.rb

Overview

The Fb adapter relies on the Fb extension.

Usage Notes

Sequence (Generator) Names

The Fb adapter supports the same approach adopted for the Oracle adapter. See ActiveRecord::Base#set_sequence_name for more details.

Note that in general there is no need to create a BEFORE INSERT trigger corresponding to a Firebird sequence generator when using ActiveRecord. In other words, you don’t have to try to make Firebird simulate an AUTO_INCREMENT or IDENTITY column. When saving a new record, ActiveRecord pre-fetches the next sequence value for the table and explicitly includes it in the INSERT statement. (Pre-fetching the next primary key value is the only reliable method for the Fb adapter to report back the id after a successful insert.)

BOOLEAN Domain

Firebird 1.5 does not provide a native BOOLEAN type. But you can easily define a BOOLEAN domain for this purpose, e.g.:

CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1));

When the Fb adapter encounters a column that is based on a domain that includes “BOOLEAN” in the domain name, it will attempt to treat the column as a BOOLEAN.

By default, the Fb adapter will assume that the BOOLEAN domain is defined as above. This can be modified if needed. For example, if you have a legacy schema with the following BOOLEAN domain defined:

CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F'));

…you can add the following line to your environment.rb file:

ActiveRecord::ConnectionAdapters::Fb.boolean_domain = { :true => 'T', :false => 'F' }

Column Name Case Semantics

Firebird and ActiveRecord have somewhat conflicting case semantics for column names.

Firebird

The standard practice is to use unquoted column names, which can be thought of as case-insensitive. (In fact, Firebird converts them to uppercase.) Quoted column names (not typically used) are case-sensitive.

ActiveRecord

Attribute accessors corresponding to column names are case-sensitive. The defaults for primary key and inheritance columns are lowercase, and in general, people use lowercase attribute names.

In order to map between the differing semantics in a way that conforms to common usage for both Firebird and ActiveRecord, uppercase column names in Firebird are converted to lowercase attribute names in ActiveRecord, and vice-versa. Mixed-case column names retain their case in both directions. Lowercase (quoted) Firebird column names are not supported. This is similar to the solutions adopted by other adapters.

In general, the best approach is to use unquoted (case-insensitive) column names in your Firebird DDL (or if you must quote, use uppercase column names). These will correspond to lowercase attributes in ActiveRecord.

For example, a Firebird table based on the following DDL:

CREATE TABLE products (
  id BIGINT NOT NULL PRIMARY KEY,
  "TYPE" VARCHAR(50),
  name VARCHAR(255) );

…will correspond to an ActiveRecord model class called Product with the following attributes: id, type, name.

Quoting "TYPE" and other Firebird reserved words:

In ActiveRecord, the default inheritance column name is type. The word type is a Firebird reserved word, so it must be quoted in any Firebird SQL statements. Because of the case mapping described above, you should always reference this column using quoted-uppercase syntax ("TYPE") within Firebird DDL or other SQL statements (as in the example above). This holds true for any other Firebird reserved words used as column names as well.

Migrations

The Fb adapter does not currently support Migrations.

Connection Options

The following options are supported by the Fb adapter.

:database

Required option. Specifies one of: (i) a Firebird database alias; (ii) the full path of a database file; or (iii) a full Firebird connection string. Do not specify :host, :service or :port as separate options when using a full connection string.

:username

Specifies the database user. Defaults to ‘sysdba’.

:password

Specifies the database password. Defaults to ‘masterkey’.

:charset

Specifies the character set to be used by the connection. Refer to the Firebird documentation for valid options.

Constant Summary collapse

@@boolean_domain =
{ :true => 1, :false => 0 }

Instance Method Summary collapse

Constructor Details

#initialize(connection, logger, connection_params = nil) ⇒ FbAdapter

Returns a new instance of FbAdapter.



195
196
197
198
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 195

def initialize(connection, logger, connection_params=nil)
  super(connection, logger)
  @connection_params = connection_params
end

Instance Method Details

#active?Boolean

CONNECTION MANAGEMENT ====================================

Returns:

  • (Boolean)


274
275
276
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 274

def active?
  @connection.open?
end

#adapter_nameObject

:nodoc:



200
201
202
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 200

def adapter_name # :nodoc:
  'Fb'
end

#add_limit_offset!(sql, options) ⇒ Object

:nodoc:



358
359
360
361
362
363
364
365
366
367
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 358

def add_limit_offset!(sql, options) # :nodoc:
  if limit = options[:limit]
    if offset = options[:offset]
      sql << " ROWS #{offset.to_i + 1} TO #{offset.to_i + limit.to_i}"
    else
      sql << " ROWS #{limit.to_i}"
    end
  end
  sql
end

#add_lock!(sql, options) ⇒ Object

:nodoc:



354
355
356
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 354

def add_lock!(sql, options) # :nodoc:
  sql
end

#begin_db_transactionObject

:nodoc:



342
343
344
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 342

def begin_db_transaction() # :nodoc:
  @transaction = @connection.transaction('READ COMMITTED')
end

#columns(table_name, name = nil) ⇒ Object

SCHEMA STATEMENTS ========================================



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 378

def columns(table_name, name = nil) # :nodoc:
  sql = <<-END_SQL
    SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
           f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
           COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
           COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
    FROM rdb$relation_fields r
    JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
    WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}'
    ORDER BY r.rdb$field_position
  END_SQL
  select_all(sql, name, :array).collect do |field|
    field_values = field.collect do |value|
      case value
        when String         then value.rstrip
        else value
      end
    end
    FbColumn.new(*field_values)
  end
end

#commit_db_transactionObject

:nodoc:



346
347
348
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 346

def commit_db_transaction() # :nodoc:
  @transaction = @connection.commit
end

#default_sequence_name(table_name, primary_key) ⇒ Object

:nodoc:



210
211
212
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 210

def default_sequence_name(table_name, primary_key) # :nodoc:
  "#{table_name}_seq"
end

#disconnect!Object



278
279
280
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 278

def disconnect!
  @connection.close rescue nil
end

#execute(sql, name = nil, &block) ⇒ Object Also known as: update, delete

:nodoc:



326
327
328
329
330
331
332
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 326

def execute(sql, name = nil, &block) # :nodoc:
  translate(sql) do |sql, args|
    log(sql, args, name) do
      @connection.execute(sql, *args, &block)
    end
  end
end

#expand(sql, args) ⇒ Object



302
303
304
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 302

def expand(sql, args)
  sql + ', ' + args * ', '
end

#indexes(table_name, name = nil) ⇒ Object

:nodoc:



404
405
406
407
408
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 404

def indexes(table_name, name = nil) #:nodoc:
  result = @connection.indexes.values.select {|ix| ix.table_name == table_name && ix.index_name !~ /^rdb\$/ }
  indexes = result.map {|ix| IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns) }
  indexes
end

#insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) ⇒ Object

:nodoc:



334
335
336
337
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 334

def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # :nodoc:
  execute(sql, name)
  id_value
end

#log(sql, args, name, &block) ⇒ Object



306
307
308
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 306

def log(sql, args, name, &block)
  super(expand(sql, args), name, &block)
end

#native_database_typesObject



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 426

def native_database_types
  {
    :primary_key => "integer not null primary key",
    :string      => { :name => "varchar", :limit => 255 },
    :text        => { :name => "blob sub_type text" },
    :integer     => { :name => "integer" },
    :float       => { :name => "float" },
    :decimal     => { :name => "decimal" },
    :datetime    => { :name => "timestamp" },
    :timestamp   => { :name => "timestamp" },
    :time        => { :name => "time" },
    :date        => { :name => "date" },
    :binary      => { :name => "blob" },
    :boolean     => { :name => "integer" }
  }
end

#next_sequence_value(sequence_name) ⇒ Object

Returns the next sequence value from a sequence generator. Not generally called directly; used by ActiveRecord to get the next primary key value when inserting a new database record (see #prefetch_primary_key?).



372
373
374
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 372

def next_sequence_value(sequence_name)
  select_one("SELECT GEN_ID(#{sequence_name}, 1) FROM RDB$DATABASE", nil, :array).first
end

#prefetch_primary_key?(table_name = nil) ⇒ Boolean

Returns true for Fb adapter (since Firebird requires primary key values to be pre-fetched before insert). See also #next_sequence_value.

Returns:

  • (Boolean)


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

def prefetch_primary_key?(table_name = nil)
  true
end

#quote(value, column = nil) ⇒ Object

QUOTING ==================================================



217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 217

def quote(value, column = nil) # :nodoc:
  case value
    when String
      "@#{Base64.encode64(value).chop}@"
    when Float, Fixnum, 
      Bignum, BigDecimal then quote_number(value)
    when Date            then quote_date(value)
    when Time, DateTime  then quote_timestamp(value)
    when NilClass        then "NULL"
    when TrueClass       then (column && column.type == :integer ? '1' : quoted_true)
    when FalseClass      then (column && column.type == :integer ? '0' : quoted_false)
    else                 quote_object(value)
  end
end

#quote_column_name(column_name) ⇒ Object

:nodoc:



259
260
261
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 259

def quote_column_name(column_name) # :nodoc:
  %Q("#{ar_to_fb_case(column_name.to_s)}")
end

#quote_date(value) ⇒ Object



237
238
239
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 237

def quote_date(value)
  "@#{Base64.encode64(value.strftime('%Y-%m-%d')).chop}@"
end

#quote_number(value) ⇒ Object



232
233
234
235
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 232

def quote_number(value)
  # "@#{Base64.encode64(value.to_s).chop}@"
  value.to_s
end

#quote_object(obj) ⇒ Object



249
250
251
252
253
254
255
256
257
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 249

def quote_object(obj)
  if obj.respond_to?(:quoted_id)
    obj.quoted_id
  elsif obj.respond_to?(:to_str)
    "@#{Base64.encode64(obj.to_str).chop}@"
  else
    "@#{Base64.encode64(obj.to_yaml).chop}@"
  end
end

#quote_string(string) ⇒ Object

:nodoc:



245
246
247
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 245

def quote_string(string) # :nodoc:
  string.gsub(/'/, "''")
end

#quote_timestamp(value) ⇒ Object



241
242
243
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 241

def quote_timestamp(value)
  "@#{Base64.encode64(value.strftime('%Y-%m-%d %H:%M:%S')).chop}@"
end

#quoted_falseObject

:nodoc:



267
268
269
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 267

def quoted_false # :nodoc:
  quote(boolean_domain[:false])
end

#quoted_trueObject

:nodoc:



263
264
265
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 263

def quoted_true # :nodoc:
  quote(boolean_domain[:true])
end

#reconnect!Object



282
283
284
285
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 282

def reconnect!
  disconnect!
  @connection = Fb::Database.connect(@connection_params)
end

#remove_index(table_name, options = {}) ⇒ Object



418
419
420
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 418

def remove_index(table_name, options = {})
  execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
end

#rename_column(table_name, column_name, new_column_name) ⇒ Object



414
415
416
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 414

def rename_column(table_name, column_name, new_column_name)
  execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TO #{new_column_name}"
end

#rollback_db_transactionObject

:nodoc:



350
351
352
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 350

def rollback_db_transaction() # :nodoc:
  @transaction = @connection.rollback
end

#select_all(sql, name = nil, format = :hash) ⇒ Object

:nodoc:



310
311
312
313
314
315
316
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 310

def select_all(sql, name = nil, format = :hash) # :nodoc:
  translate(sql) do |sql, args|
    log(sql, args, name) do
      @connection.query(format, sql, *args)
    end
  end
end

#select_one(sql, name = nil, format = :hash) ⇒ Object

:nodoc:



318
319
320
321
322
323
324
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 318

def select_one(sql, name = nil, format = :hash) # :nodoc:
  translate(sql) do |sql, args|
    log(sql, args, name) do
      @connection.query(format, sql, *args).first
    end
  end
end

#supports_migrations?Boolean

Returns:

  • (Boolean)


422
423
424
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 422

def supports_migrations?
  false
end

#table_alias_lengthObject



410
411
412
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 410

def table_alias_length
  255
end

#tables(name = nil) ⇒ Object



400
401
402
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 400

def tables(name = nil)
  @connection.table_names.map {|t| t.downcase }
end

#translate(sql) {|sql, args| ... } ⇒ Object

DATABASE STATEMENTS ======================================

Yields:

  • (sql, args)


289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/active_record/connection_adapters/fb_adapter.rb', line 289

def translate(sql)
  sql.gsub!(/\bIN\s+\(NULL\)/i, 'IS NULL')
  sql.sub!(/\bWHERE\s.*$/im) do |m|
    m.gsub(/\s=\s*NULL\b/i, ' IS NULL')
  end
  sql.gsub!(/\sIN\s+\([^\)]*\)/mi) do |m|
    m.gsub(/\(([^\)]*)\)/m) { |n| n.gsub(/\@(.*?)\@/m) { |n| "'#{quote_string(Base64.decode64(n[1..-1]))}'" } }
  end
  args = []
  sql.gsub!(/\@(.*?)\@/m) { |m| args << Base64.decode64(m[1..-1]); '?' }
  yield(sql, args) if block_given?
end