Class: ActiveRecord::ConnectionAdapters::RubyfbAdapter

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

Overview

The Firebird adapter relies on the FireRuby extension, version 0.4.0 or later (available as a gem or from RubyForge). FireRuby works with Firebird 1.5.x on Linux, OS X and Win32 platforms.

Usage Notes

Sequence (Generator) Names

The Firebird 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 Firebird 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) OR VALUE IS NULL);

When the Firebird 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 Firebird 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::RubyfbAdapter.boolean_domain = { :true => 'T', :false => 'F' }

BLOB Elements

The Firebird adapter currently provides only limited support for BLOB columns. You cannot currently retrieve a BLOB as an IO stream. When selecting a BLOB, the entire element is converted into a String. BLOB handling is supported by writing an empty BLOB to the database on insert/update and then executing a second query to save the BLOB.

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 unqouted (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 Firebird Adapter now supports Migrations.

Create/Drop Table and Sequence Generators

Creating or dropping a table will automatically create/drop a correpsonding sequence generator, using the default naming convension. You can specify a different name using the :sequence option; no generator is created if :sequence is set to false.

Rename Table

The Firebird #rename_table Migration should be used with caution. Firebird 1.5 lacks built-in support for this feature, so it is implemented by making a copy of the original table (including column definitions, indexes and data records), and then dropping the original table. Constraints and Triggers are not properly copied, so avoid this method if your original table includes constraints (other than the primary key) or triggers. (Consider manually copying your table or using a view instead.)

Connection Options

The following options are supported by the Firebird adapter. None of the options have default values.

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

:host

Set to "remote.host.name" for remote database connections. May be omitted for local connections if a full database path is specified for :database. Some platforms require a value of "localhost" for local connections when using a Firebird database alias.

:service

Specifies a service name for the connection. Only used if :host is provided. Required when connecting to a non-standard service.

:port

Specifies the connection port. Only used if :host is provided and :service is not. Required when connecting to a non-standard port and :service is not defined.

:username

Specifies the database user. May be omitted or set to nil (together with :password) to use the underlying operating system user credentials on supported platforms.

:password

Specifies the database password. Must be provided if :username is explicitly specified; should be omitted if OS user credentials are are being used.

:charset

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

Constant Summary collapse

TEMP_COLUMN_NAME =
'AR$TEMP_COLUMN'
@@boolean_domain =
{ :name => "d_boolean", :type => "smallint", :true => 1, :false => 0 }

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of RubyfbAdapter.



270
271
272
273
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 270

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

Instance Method Details

#active?Boolean

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

Returns:

  • (Boolean)


349
350
351
352
353
354
355
356
357
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 349

def active? # :nodoc:
	return false if @connection.closed?
	begin
		execute('select first 1 cast(1 as smallint) from rdb$database')
		true
	rescue
		false
	end
end

#adapter_nameObject

:nodoc:



275
276
277
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 275

def adapter_name # :nodoc:
  'Rubyfb'
end

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

:nodoc:



516
517
518
519
520
521
522
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 516

def add_column(table_name, column_name, type, options = {}) # :nodoc:
  super
rescue StatementInvalid
  raise unless non_existent_domain_error?
  create_boolean_domain
  super
end

#add_limit_offset!(sql, options) ⇒ Object

:nodoc:



400
401
402
403
404
405
406
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 400

def add_limit_offset!(sql, options) # :nodoc:
  if options[:limit]
    limit_string = "FIRST #{options[:limit]}"
    limit_string << " SKIP #{options[:offset]}" if options[:offset]
    sql.sub!(/\A(\s*SELECT\s)/i, '\&' + limit_string + ' ')
  end
end

#begin_db_transactionObject

:nodoc:



384
385
386
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 384

def begin_db_transaction() # :nodoc:
  @transaction = @connection.start_transaction
end

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

:nodoc:



524
525
526
527
528
529
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 524

def change_column(table_name, column_name, type, options = {}) # :nodoc:
  change_column_type(table_name, column_name, type, options)
  change_column_position(table_name, column_name, options[:position]) if options.include?(:position)
  change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
  change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
end

#change_column_default(table_name, column_name, default) ⇒ Object

:nodoc:



531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 531

def change_column_default(table_name, column_name, default) # :nodoc:
  table_name = table_name.to_s.upcase
  sql = <<-end_sql
    UPDATE rdb$relation_fields f1
    SET f1.rdb$default_source =
          (SELECT f2.rdb$default_source FROM rdb$relation_fields f2
           WHERE f2.rdb$relation_name = '#{table_name}'
           AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}'),
        f1.rdb$default_value =
          (SELECT f2.rdb$default_value FROM rdb$relation_fields f2
           WHERE f2.rdb$relation_name = '#{table_name}'
           AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}')
    WHERE f1.rdb$relation_name = '#{table_name}'
    AND f1.rdb$field_name = '#{ar_to_fb_case(column_name.to_s)}'
  end_sql
  transaction do
    add_column(table_name, TEMP_COLUMN_NAME, :string, :default => default)
    execute_statement(sql)
    remove_column(table_name, TEMP_COLUMN_NAME)
  end
end

#change_column_null(table_name, column_name, null, default = nil) ⇒ Object



553
554
555
556
557
558
559
560
561
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 553

def change_column_null(table_name, column_name, null, default = nil)
  table_name = table_name.to_s.upcase
  column_name = column_name.to_s.upcase

  unless null || default.nil?
    execute_statement("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
  end
  execute_statement("UPDATE RDB$RELATION_FIELDS SET RDB$NULL_FLAG = #{null ? 'null' : '1'} WHERE (RDB$FIELD_NAME = '#{column_name}') and (RDB$RELATION_NAME = '#{table_name}')")
end

#columns(table_name, name = nil) ⇒ Object

:nodoc:



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 471

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_rows(sql, name).collect do |row|
    field_values = row.collect do |value|
      case value
        when String then value.rstrip
        else value
      end
    end
    FirebirdColumn.new(self, *field_values)
  end
end

#commit_db_transactionObject

:nodoc:



388
389
390
391
392
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 388

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

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

:nodoc:



494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 494

def create_table(name, options = {}) # :nodoc:
  begin
    super
  rescue StatementInvalid
    raise unless non_existent_domain_error?
    create_boolean_domain
    super
  end
  unless options[:id] == false or options[:sequence] == false
    sequence_name = options[:sequence] || default_sequence_name(name)
    create_sequence(sequence_name)
  end
end

#current_databaseObject

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



442
443
444
445
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 442

def current_database # :nodoc:
  file = @connection.database.file.split(':').last
  File.basename(file, '.*')
end

#default_sequence_name(table_name, primary_key = nil) ⇒ Object

:nodoc:



307
308
309
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 307

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

#disconnect!Object

:nodoc:



359
360
361
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 359

def disconnect! # :nodoc:
  @connection.close rescue nil
end

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

:nodoc:



508
509
510
511
512
513
514
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 508

def drop_table(name, options = {}) # :nodoc:
  super(name)
  unless options[:sequence] == false
    sequence_name = options[:sequence] || default_sequence_name(name)
    drop_sequence(sequence_name) if sequence_exists?(sequence_name)
  end
end

#dump_schema_informationObject

:nodoc:



593
594
595
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 593

def dump_schema_information # :nodoc:
  super << ";\n"
end

#execute(sql, name = nil, &block) ⇒ Object

:nodoc:



375
376
377
378
379
380
381
382
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 375

def execute(sql, name = nil, &block) # :nodoc:
  exec_result = execute_statement(sql, name, &block)
  if exec_result.instance_of?(FireRuby::ResultSet)
    exec_result.close
    exec_result = nil
  end
  return exec_result
end

#indexes(table_name, name = nil) ⇒ Object

:nodoc:



461
462
463
464
465
466
467
468
469
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 461

def indexes(table_name, name = nil) # :nodoc:
  (table_name, false, name).inject([]) do |indexes, row|
    if indexes.empty? or indexes.last.name != row[0]
      indexes << IndexDefinition.new(table_name, row[0].rstrip.downcase, row[1] == 1, [])
    end
    indexes.last.columns << row[2].rstrip.downcase
    indexes
  end
end

#insert_fixture(fixture, table_name) ⇒ Object

Inserts the given fixture into the table. Overridden to properly handle blobs.



416
417
418
419
420
421
422
423
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 416

def insert_fixture(fixture, table_name)
  super

  klass = fixture.class_name.constantize rescue nil
  if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
    write_blobs(table_name, klass, fixture)
  end
end

#native_database_typesObject

:nodoc:



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 283

def native_database_types # :nodoc:
  {
    :primary_key => "BIGINT NOT NULL PRIMARY KEY",
    :string      => { :name => "varchar", :limit => 255 },
    :text        => { :name => "blob sub_type text" },
    :integer     => { :name => "bigint" },
    :decimal     => { :name => "decimal" },
    :numeric     => { :name => "numeric" },
    :float       => { :name => "float" },
    :datetime    => { :name => "timestamp" },
    :timestamp   => { :name => "timestamp" },
    :time        => { :name => "time" },
    :date        => { :name => "date" },
    :binary      => { :name => "blob sub_type 0" },
    :boolean     => boolean_domain
  }
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?).



411
412
413
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 411

def next_sequence_value(sequence_name)
  FireRuby::Generator.new(sequence_name, @connection).next(1)
end

#prefetch_primary_key?(table_name = nil) ⇒ Boolean

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

Returns:

  • (Boolean)


303
304
305
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 303

def prefetch_primary_key?(table_name = nil)
  true
end

#quote(value, column = nil) ⇒ Object

We use quoting in order to implement BLOB handling. In order to do this we quote a BLOB to an empty string which will force Firebird to create an empty BLOB in the db for us. Quoting is used in some other places besides insert/update like for column defaults. That is why we are checking caller to see where we’re coming from. This isn’t perfect but It works.



320
321
322
323
324
325
326
327
328
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 320

def quote(value, column = nil) # :nodoc:
  if [Time, DateTime].include?(value.class)
    "CAST('#{value.strftime("%Y-%m-%d %H:%M:%S")}' AS TIMESTAMP)"
  elsif value && column && [:text, :binary].include?(column.type) && caller.to_s !~ /add_column_options!/i 
    "''"
  else
    super
  end
end

#quote_column_name(column_name) ⇒ Object

:nodoc:



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

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

#quote_string(string) ⇒ Object

:nodoc:



330
331
332
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 330

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

#quoted_falseObject

:nodoc:



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

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

#quoted_trueObject

:nodoc:



338
339
340
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 338

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

#reconnect!Object

:nodoc:



363
364
365
366
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 363

def reconnect! # :nodoc:
  disconnect!
  @connection = @connection.database.connect(*@connection_params)
end

#recreate_database!Object

:nodoc:



447
448
449
450
451
452
453
454
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 447

def recreate_database! # :nodoc:
  sql = "SELECT rdb$character_set_name FROM rdb$database"
  charset = select_rows(sql).first[0].rstrip
  disconnect!
  @connection.database.drop(*@connection_params)
  FireRuby::Database.create(@connection.database.file,
    @connection_params[0], @connection_params[1], 4096, charset)
end

#remove_index(table_name, options) ⇒ Object

:nodoc:



567
568
569
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 567

def remove_index(table_name, options) #:nodoc:
  execute_statement("DROP INDEX #{quote_column_name(index_name(table_name, options))}")
end

#rename_column(table_name, column_name, new_column_name) ⇒ Object

:nodoc:



563
564
565
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 563

def rename_column(table_name, column_name, new_column_name) # :nodoc:
  execute_statement("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}")
end

#rename_table(name, new_name) ⇒ Object

:nodoc:



571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 571

def rename_table(name, new_name) # :nodoc:
  if table_has_constraints_or_dependencies?(name)
    raise ActiveRecordError,
      "Table #{name} includes constraints or dependencies that are not supported by " <<
      "the Firebird rename_table migration. Try explicitly removing the constraints/" <<
      "dependencies first, or manually renaming the table."
  end

  transaction do
    copy_table(name, new_name)
    copy_table_indexes(name, new_name)
  end
  begin
    copy_table_data(name, new_name)
    copy_sequence_value(name, new_name)
  rescue
    drop_table(new_name)
    raise
  end
  drop_table(name)
end

#rollback_db_transactionObject

:nodoc:



394
395
396
397
398
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 394

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

#select_rows(sql, name = nil) ⇒ Object

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



371
372
373
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 371

def select_rows(sql, name = nil)
  select_raw(sql, name).last
end

#supports_migrations?Boolean

:nodoc:

Returns:

  • (Boolean)


279
280
281
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 279

def supports_migrations? # :nodoc:
  true
end

#tables(name = nil) ⇒ Object

:nodoc:



456
457
458
459
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 456

def tables(name = nil) # :nodoc:
  sql = "SELECT rdb$relation_name FROM rdb$relations WHERE rdb$system_flag = 0"
  select_rows(sql, name).collect { |row| row[0].rstrip.downcase }
end

#type_to_sql(type, limit = nil, precision = nil, scale = nil) ⇒ Object

:nodoc:



597
598
599
600
601
602
603
604
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 597

def type_to_sql(type, limit = nil, precision = nil, scale = nil) # :nodoc:
  case type
    when :integer then integer_sql_type(limit)
    when :float   then float_sql_type(limit)
    when :string  then super(type, limit, precision, scale)
    else super(type, limit, precision, scale)
  end
end

#write_blobs(table_name, klass, attributes) ⇒ Object

Writes BLOB values from attributes, as indicated by the BLOB columns of klass.



426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/active_record/connection_adapters/rubyfb_adapter.rb', line 426

def write_blobs(table_name, klass, attributes)
  id = quote(attributes[klass.primary_key])
  klass.columns.select { |col| col.sql_type =~ /BLOB$/i }.each do |col|
    value = attributes[col.name]
    value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
    value = value.read if value.respond_to?(:read)
    next if value.nil?  || (value == '')
    s = FireRuby::Statement.new(@connection, @transaction, "UPDATE #{table_name} set #{col.name} = ? WHERE #{klass.primary_key} = #{id}", 3)       
    s.execute_for([value.to_s])
    s.close
  end
end