activerecord-fb-adapter 0.8.9 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +82 -0
- data/lib/active_record/connection_adapters/fb/database_limits.rb +42 -0
- data/lib/active_record/connection_adapters/fb/database_statements.rb +127 -0
- data/lib/active_record/connection_adapters/fb/quoting.rb +103 -0
- data/lib/active_record/connection_adapters/fb/schema_statements.rb +247 -0
- data/lib/active_record/connection_adapters/fb/table_definition.rb +14 -0
- data/lib/active_record/connection_adapters/fb_adapter.rb +43 -760
- data/lib/active_record/connection_adapters/fb_column.rb +73 -0
- data/lib/active_record/fb_base.rb +25 -0
- data/lib/arel/visitors/fb.rb +43 -0
- metadata +35 -15
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2678bd6b3b5635e1bdae1638babacb97d42af498
|
4
|
+
data.tar.gz: ee672fe8d0e86cc4ae2e86b633492cc4d7db888e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fc9b1a2711bfca2a744eb83066f3b79ca96f4ddfd64f302c656cd77ec023fa1484b848d028a2f4c90daa6474fa3058ebc13dcdecb4a70a73d0815e38c7a64e1c
|
7
|
+
data.tar.gz: d720204fe3cca4a0689089c7841f329776e4bd693b9ca536cf70e1042fc04ec4a294b745a615d71b062e52335cbb8d482ae1f3bd651db62c28f9c1edc453cdb4
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# ActiveRecord Firebird Adapter
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/activerecord-fb-adapter.svg)](http://badge.fury.io/rb/activerecord-fb-adapter)
|
3
|
+
|
4
|
+
<img src="/project_logo.png" align="left" hspace="10">
|
5
|
+
This is the ActiveRecord adapter for working with the [Firebird SQL Server](http://firebirdsql.org/). It currently supports Rails 3.2.x and 4.x. Although this adapter may not yet have feature parity with the 1st tier databases supported by Rails, it has been used in production by different people for several months without issues and may be considered stable. It uses under the hood the [Ruby Firebird Extension Library](https://github.com/rowland/fb).
|
6
|
+
|
7
|
+
## What's supported
|
8
|
+
|
9
|
+
- Datatypes: string, integer, datetime, boolean, float, decimal, text (blob).
|
10
|
+
- Rails migrations and db/schema.rb generation.
|
11
|
+
- Linux, OS X, and Windows supported.
|
12
|
+
|
13
|
+
## Getting started
|
14
|
+
|
15
|
+
1) Install and start the Firebird Server in your machine (varies across operating systems).
|
16
|
+
|
17
|
+
2) Create a new Rails project.
|
18
|
+
|
19
|
+
```
|
20
|
+
rails new firebird_test
|
21
|
+
cd firebird_test
|
22
|
+
```
|
23
|
+
|
24
|
+
3) Edit the project Gemfile and add the **activerecord-fb-adapter** gem:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'activerecord-fb-adapter'
|
28
|
+
```
|
29
|
+
|
30
|
+
Then run:
|
31
|
+
|
32
|
+
```
|
33
|
+
bundle update
|
34
|
+
```
|
35
|
+
|
36
|
+
which will make bundler to get the gem with it's only dependency: the Fb gem which is "native" (has C code) and will be compiled the first time. Be sure you have a Firebird installation with access to the "ibase.h" file for this to succeed.
|
37
|
+
|
38
|
+
4) Edit the **database.yml** for configuring your database connection:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
development:
|
42
|
+
adapter: fb
|
43
|
+
database: db/development.fdb
|
44
|
+
username: SYSDBA
|
45
|
+
password: masterkey
|
46
|
+
host: localhost
|
47
|
+
encoding: UTF-8
|
48
|
+
create: true
|
49
|
+
```
|
50
|
+
|
51
|
+
The default Firebird administrator username and password are **SYSDBA** and **masterkey**, you may have to adjust this to your installation.
|
52
|
+
|
53
|
+
Currently the adapter does not supports the "rake db:create" task, so in order to create the database you must add the "create: true" option; with this switch the first time the adapter tries to connect to the database it will be created if it doesn't exists.
|
54
|
+
|
55
|
+
5) Start the rails server in development mode
|
56
|
+
|
57
|
+
```
|
58
|
+
bundle exec rails server
|
59
|
+
```
|
60
|
+
|
61
|
+
Open your browser at http://localhost:3000, this will create the database on the first connection.
|
62
|
+
|
63
|
+
On Linux you may get:
|
64
|
+
|
65
|
+
```
|
66
|
+
Fb::Error (Unsuccessful execution caused by a system error that precludes successful execution of subsequent statements
|
67
|
+
I/O error during "open O_CREAT" operation for file "db/development.fdb"
|
68
|
+
Error while trying to create file
|
69
|
+
Permission denied
|
70
|
+
```
|
71
|
+
|
72
|
+
This is because, by default, the Firebird Server runs under the "firebird" user and group, which has no write access to the "db" folder of the project. To fix it run:
|
73
|
+
|
74
|
+
```
|
75
|
+
chmod o+w db
|
76
|
+
```
|
77
|
+
which will add write permission to "others" group.
|
78
|
+
|
79
|
+
6) Now you can start generating scaffolds or models and rails will create the corresponding migrations. Use **bundle exec rake db:migrate** and **bundle exec rake db:rollback** for migrating the database; this will update your **db/schema.rb** file automatically.
|
80
|
+
|
81
|
+
## License
|
82
|
+
It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Fb
|
4
|
+
module DatabaseLimits
|
5
|
+
# the maximum length of a table alias
|
6
|
+
def table_alias_length
|
7
|
+
31
|
8
|
+
end
|
9
|
+
|
10
|
+
# the maximum length of a column name
|
11
|
+
def column_name_length
|
12
|
+
31
|
13
|
+
end
|
14
|
+
|
15
|
+
# the maximum length of a table name
|
16
|
+
def table_name_length
|
17
|
+
31
|
18
|
+
end
|
19
|
+
|
20
|
+
# the maximum length of an index name
|
21
|
+
def index_name_length
|
22
|
+
31
|
23
|
+
end
|
24
|
+
|
25
|
+
# the maximum number of indexes per table
|
26
|
+
def indexes_per_table
|
27
|
+
65_535
|
28
|
+
end
|
29
|
+
|
30
|
+
# the maximum number of elements in an IN (x,y,z) clause
|
31
|
+
def in_clause_length
|
32
|
+
1_499
|
33
|
+
end
|
34
|
+
|
35
|
+
# the maximum length of an SQL query
|
36
|
+
def sql_query_length
|
37
|
+
32_767
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Fb
|
4
|
+
module DatabaseStatements
|
5
|
+
# Returns an array of arrays containing the field values.
|
6
|
+
# Order is the same as that returned by +columns+.
|
7
|
+
def select_rows(sql, name = nil, binds = [])
|
8
|
+
exec_query(sql, name, binds).to_a.map(&:values)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Executes the SQL statement in the context of this connection.
|
12
|
+
def execute(sql, name = nil, skip_logging = false)
|
13
|
+
translate(sql) do |translated, args|
|
14
|
+
if (name == :skip_logging) || skip_logging
|
15
|
+
@connection.execute(translated, *args)
|
16
|
+
else
|
17
|
+
log(sql, args, name) do
|
18
|
+
@connection.execute(translated, *args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Executes +sql+ statement in the context of this connection using
|
25
|
+
# +binds+ as the bind substitutes. +name+ is logged along with
|
26
|
+
# the executed +sql+ statement.
|
27
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
28
|
+
translate(sql, binds) do |translated, args|
|
29
|
+
log(expand(translated, args), name) do
|
30
|
+
result, rows = @connection.execute(translated, *args) do |cursor|
|
31
|
+
[cursor.fields, cursor.fetchall]
|
32
|
+
end
|
33
|
+
next result unless result.respond_to?(:map)
|
34
|
+
cols = result.map { |col| col.name }
|
35
|
+
ActiveRecord::Result.new(cols, rows)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def explain(arel, binds = [])
|
41
|
+
to_sql(arel, binds)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks whether there is currently no transaction active. This is done
|
45
|
+
# by querying the database driver, and does not use the transaction
|
46
|
+
# house-keeping information recorded by #increment_open_transactions and
|
47
|
+
# friends.
|
48
|
+
#
|
49
|
+
# Returns true if there is no transaction active, false if there is a
|
50
|
+
# transaction active, and nil if this information is unknown.
|
51
|
+
def outside_transaction?
|
52
|
+
!@connection.transaction_started
|
53
|
+
end
|
54
|
+
|
55
|
+
# Begins the transaction (and turns off auto-committing).
|
56
|
+
def begin_db_transaction
|
57
|
+
@connection.transaction('READ COMMITTED')
|
58
|
+
end
|
59
|
+
|
60
|
+
# Commits the transaction (and turns on auto-committing).
|
61
|
+
def commit_db_transaction
|
62
|
+
@connection.commit
|
63
|
+
end
|
64
|
+
|
65
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
66
|
+
# done if the transaction block raises an exception or returns false.
|
67
|
+
def rollback_db_transaction
|
68
|
+
@connection.rollback
|
69
|
+
end
|
70
|
+
|
71
|
+
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
|
72
|
+
# fragment that has the same semantics as LIMIT and OFFSET.
|
73
|
+
#
|
74
|
+
# +options+ must be a Hash which contains a +:limit+ option
|
75
|
+
# and an +:offset+ option.
|
76
|
+
#
|
77
|
+
# This method *modifies* the +sql+ parameter.
|
78
|
+
#
|
79
|
+
# ===== Examples
|
80
|
+
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
|
81
|
+
# generates
|
82
|
+
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
|
83
|
+
def add_limit_offset!(sql, options) # :nodoc:
|
84
|
+
if limit = options[:limit]
|
85
|
+
if offset = options[:offset]
|
86
|
+
sql << " ROWS #{offset.to_i + 1} TO #{offset.to_i + limit.to_i}"
|
87
|
+
else
|
88
|
+
sql << " ROWS #{limit.to_i}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
sql
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_sequence_name(table_name, _column = nil)
|
95
|
+
"#{table_name.to_s.tr('-', '_')[0, table_name_length - 4]}_seq"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set the sequence to the max value of the table's column.
|
99
|
+
def reset_sequence!(table, column, sequence = nil)
|
100
|
+
sequence ||= default_sequence_name(table, column)
|
101
|
+
max_id = select_value("select max(#{column}) from #{table}")
|
102
|
+
execute("alter sequence #{sequence} restart with #{max_id}")
|
103
|
+
end
|
104
|
+
|
105
|
+
# Uses the raw connection to get the next sequence value.
|
106
|
+
def next_sequence_value(sequence_name)
|
107
|
+
@connection.query("SELECT NEXT VALUE FOR #{sequence_name} FROM RDB$DATABASE")[0][0]
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
# Returns an array of record hashes with the column names as keys and
|
113
|
+
# column values as values. ActiveRecord >= 4 returns an ActiveRecord::Result.
|
114
|
+
def select(sql, name = nil, binds = [])
|
115
|
+
result = exec_query(sql, name, binds)
|
116
|
+
ActiveRecord::VERSION::MAJOR > 3 ? result : result.to_a
|
117
|
+
end
|
118
|
+
|
119
|
+
# Since the ID is prefetched and passed to #insert, this method is useless.
|
120
|
+
# Overriding this method allows us to avoid overriding #insert.
|
121
|
+
def last_inserted_id(_result)
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Fb
|
4
|
+
module Quoting
|
5
|
+
def quote(value, column = nil)
|
6
|
+
# records are quoted as their primary key
|
7
|
+
return value.quoted_id if value.respond_to?(:quoted_id)
|
8
|
+
type = column && column.type
|
9
|
+
|
10
|
+
case value
|
11
|
+
when String, ActiveSupport::Multibyte::Chars
|
12
|
+
value = value.to_s
|
13
|
+
if [:integer, :float].include?(type)
|
14
|
+
value = type == :integer ? value.to_i : value.to_f
|
15
|
+
value.to_s
|
16
|
+
elsif type && type != :binary && value.size < 256 && !value.include?('@')
|
17
|
+
"'#{quote_string(value)}'"
|
18
|
+
else
|
19
|
+
"@#{Base64.encode64(value).chop}@"
|
20
|
+
end
|
21
|
+
when nil then "NULL"
|
22
|
+
when true then quoted_true
|
23
|
+
when false then quoted_false
|
24
|
+
when Numeric, ActiveSupport::Duration then value.to_s
|
25
|
+
# BigDecimals need to be output in a non-normalized form and quoted.
|
26
|
+
when BigDecimal then value.to_s('F')
|
27
|
+
when Symbol then "'#{quote_string(value.to_s)}'"
|
28
|
+
when Class then "'#{value}'"
|
29
|
+
else
|
30
|
+
if value.acts_like?(:date)
|
31
|
+
quote_date(value)
|
32
|
+
elsif value.acts_like?(:time)
|
33
|
+
quote_timestamp(value)
|
34
|
+
else
|
35
|
+
quote_object(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def quote_date(value)
|
41
|
+
"@#{Base64.encode64(value.strftime('%Y-%m-%d')).chop}@"
|
42
|
+
end
|
43
|
+
|
44
|
+
def quote_timestamp(value)
|
45
|
+
get = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
46
|
+
value = value.respond_to?(get) ? value.send(get) : value
|
47
|
+
"@#{Base64.encode64(value.strftime('%Y-%m-%d %H:%M:%S')).chop}@"
|
48
|
+
end
|
49
|
+
|
50
|
+
def quote_string(string) # :nodoc:
|
51
|
+
string.gsub(/'/, "''")
|
52
|
+
end
|
53
|
+
|
54
|
+
def quote_object(obj)
|
55
|
+
if obj.respond_to?(:to_str)
|
56
|
+
"@#{Base64.encode64(obj.to_str).chop}@"
|
57
|
+
else
|
58
|
+
"@#{Base64.encode64(obj.to_yaml).chop}@"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def quote_column_name(column_name) # :nodoc:
|
63
|
+
if @connection.dialect == 1
|
64
|
+
%Q(#{ar_to_fb_case(column_name.to_s)})
|
65
|
+
else
|
66
|
+
%Q("#{ar_to_fb_case(column_name.to_s)}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def quote_table_name_for_assignment(_table, attr)
|
71
|
+
quote_column_name(attr)
|
72
|
+
end if ::ActiveRecord::VERSION::MAJOR >= 4
|
73
|
+
|
74
|
+
def quoted_true # :nodoc:
|
75
|
+
quote(boolean_domain[:true])
|
76
|
+
end
|
77
|
+
|
78
|
+
def quoted_false # :nodoc:
|
79
|
+
quote(boolean_domain[:false])
|
80
|
+
end
|
81
|
+
|
82
|
+
def type_cast(value, column)
|
83
|
+
return super unless value == true || value == false
|
84
|
+
value ? quoted_true : quoted_false
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Maps uppercase Firebird column names to lowercase for ActiveRecord;
|
90
|
+
# mixed-case columns retain their original case.
|
91
|
+
def fb_to_ar_case(column_name)
|
92
|
+
column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase
|
93
|
+
end
|
94
|
+
|
95
|
+
# Maps lowercase ActiveRecord column names to uppercase for Fierbird;
|
96
|
+
# mixed-case columns retain their original case.
|
97
|
+
def ar_to_fb_case(column_name)
|
98
|
+
column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Fb
|
4
|
+
module SchemaStatements
|
5
|
+
# Returns a Hash of mappings from the abstract data types to the native
|
6
|
+
# database types. See TableDefinition#column for details on the recognized
|
7
|
+
# abstract data types.
|
8
|
+
def native_database_types
|
9
|
+
{
|
10
|
+
:primary_key => 'integer not null primary key',
|
11
|
+
:string => { :name => 'varchar', :limit => 255 },
|
12
|
+
:text => { :name => 'blob sub_type text' },
|
13
|
+
:integer => { :name => 'integer' },
|
14
|
+
:float => { :name => 'float' },
|
15
|
+
:decimal => { :name => 'decimal' },
|
16
|
+
:datetime => { :name => 'timestamp' },
|
17
|
+
:timestamp => { :name => 'timestamp' },
|
18
|
+
:time => { :name => 'time' },
|
19
|
+
:date => { :name => 'date' },
|
20
|
+
:binary => { :name => 'blob' },
|
21
|
+
:boolean => { :name => boolean_domain[:name] }
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def tables(_name = nil)
|
26
|
+
@connection.table_names
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an array of indexes for the given table.
|
30
|
+
def indexes(table_name, _name = nil)
|
31
|
+
@connection.indexes.values.map { |ix|
|
32
|
+
if ix.table_name == table_name && ix.index_name !~ /^rdb\$/
|
33
|
+
IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns)
|
34
|
+
end
|
35
|
+
}.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
def primary_key(table_name) #:nodoc:
|
39
|
+
sql = <<-END_SQL
|
40
|
+
SELECT s.rdb$field_name
|
41
|
+
FROM rdb$indices i
|
42
|
+
JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
|
43
|
+
LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
|
44
|
+
WHERE i.rdb$relation_name = '#{ar_to_fb_case(table_name)}' and c.rdb$constraint_type = 'PRIMARY KEY';
|
45
|
+
END_SQL
|
46
|
+
row = select_one(sql)
|
47
|
+
row && fb_to_ar_case(row.values.first.rstrip)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns an array of Column objects for the table specified by +table_name+.
|
51
|
+
# See the concrete implementation for details on the expected parameter values.
|
52
|
+
def columns(table_name, name = nil)
|
53
|
+
sql = <<-END_SQL
|
54
|
+
SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
|
55
|
+
f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
|
56
|
+
COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
|
57
|
+
COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
|
58
|
+
FROM rdb$relation_fields r
|
59
|
+
JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
|
60
|
+
WHERE r.rdb$relation_name = '#{ar_to_fb_case(table_name)}'
|
61
|
+
ORDER BY r.rdb$field_position
|
62
|
+
END_SQL
|
63
|
+
select_rows(sql, name).map do |field|
|
64
|
+
FbColumn.new(*field.map { |value|
|
65
|
+
value.is_a?(String) ? value.rstrip : value
|
66
|
+
})
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_table(name, options = {}) # :nodoc:
|
71
|
+
needs_sequence = options[:id] != false
|
72
|
+
while_ensuring_boolean_domain do
|
73
|
+
super(name, options) do |table_def|
|
74
|
+
yield table_def if block_given?
|
75
|
+
needs_sequence ||= table_def.needs_sequence
|
76
|
+
end
|
77
|
+
end
|
78
|
+
return if options[:sequence] == false || !needs_sequence
|
79
|
+
create_sequence(options[:sequence] || default_sequence_name(name))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Unfortunately, this is a limitation of Firebird.
|
83
|
+
def rename_table(name, new_name)
|
84
|
+
fail 'Firebird does not support renaming tables.'
|
85
|
+
end
|
86
|
+
|
87
|
+
def drop_table(name, options = {}) # :nodoc:
|
88
|
+
super(name)
|
89
|
+
return if options[:sequence] == false
|
90
|
+
sequence_name = options[:sequence] || default_sequence_name(name)
|
91
|
+
drop_sequence(sequence_name) if sequence_exists?(sequence_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Creates a sequence
|
95
|
+
# ===== Examples
|
96
|
+
# create_sequence('DOGS_SEQ')
|
97
|
+
def create_sequence(sequence_name)
|
98
|
+
execute("CREATE SEQUENCE #{sequence_name}") rescue nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Drops a sequence
|
102
|
+
# ===== Examples
|
103
|
+
# drop_sequence('DOGS_SEQ')
|
104
|
+
def drop_sequence(sequence_name)
|
105
|
+
execute("DROP SEQUENCE #{sequence_name}") rescue nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Adds a new column to the named table.
|
109
|
+
# See TableDefinition#column for details of the options you can use.
|
110
|
+
def add_column(table_name, column_name, type, options = {})
|
111
|
+
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
112
|
+
add_column_options!(add_column_sql, options)
|
113
|
+
while_ensuring_boolean_domain { execute(add_column_sql) }
|
114
|
+
if type == :primary_key && options[:sequence] != false
|
115
|
+
create_sequence(options[:sequence] || default_sequence_name(table_name))
|
116
|
+
end
|
117
|
+
return unless options[:position]
|
118
|
+
# position is 1-based but add 1 to skip id column
|
119
|
+
alter_position_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} POSITION #{options[:position] + 1}"
|
120
|
+
execute(alter_position_sql)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Changes the column's definition according to the new options.
|
124
|
+
# See TableDefinition#column for details of the options you can use.
|
125
|
+
# ===== Examples
|
126
|
+
# change_column(:suppliers, :name, :string, :limit => 80)
|
127
|
+
# change_column(:accounts, :description, :text)
|
128
|
+
def change_column(table_name, column_name, type, options = {})
|
129
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
130
|
+
execute(sql)
|
131
|
+
change_column_null(table_name, column_name, !!options[:null]) if options.key?(:null)
|
132
|
+
change_column_default(table_name, column_name, options[:default]) if options[:default]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Sets a new default value for a column. If you want to set the default
|
136
|
+
# value to +NULL+, you are out of luck. You need to
|
137
|
+
# DatabaseStatements#execute the appropriate SQL statement yourself.
|
138
|
+
# ===== Examples
|
139
|
+
# change_column_default(:suppliers, :qualification, 'new')
|
140
|
+
# change_column_default(:accounts, :authorized, 1)
|
141
|
+
def change_column_default(table_name, column_name, default)
|
142
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}")
|
143
|
+
end
|
144
|
+
|
145
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
146
|
+
fail 'Firebird cannot change the nullability of a column using ALTER COLUMN.'
|
147
|
+
end
|
148
|
+
|
149
|
+
# Renames a column.
|
150
|
+
# ===== Example
|
151
|
+
# rename_column(:suppliers, :description, :name)
|
152
|
+
def rename_column(table_name, column_name, new_column_name)
|
153
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
154
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
155
|
+
end
|
156
|
+
|
157
|
+
def remove_index!(_table_name, index_name) #:nodoc:
|
158
|
+
execute("DROP INDEX #{quote_column_name(index_name)}")
|
159
|
+
end
|
160
|
+
|
161
|
+
def index_name(table_name, options) #:nodoc:
|
162
|
+
if options.respond_to?(:keys) # legacy support
|
163
|
+
if options[:column]
|
164
|
+
"#{table_name}_#{Array.wrap(options[:column]) * '_'}"
|
165
|
+
elsif options[:name]
|
166
|
+
options[:name]
|
167
|
+
else
|
168
|
+
fail ArgumentError, "You must specify the index name"
|
169
|
+
end
|
170
|
+
else
|
171
|
+
index_name(table_name, :column => options)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
176
|
+
case type
|
177
|
+
when :integer then integer_to_sql(limit)
|
178
|
+
when :float then float_to_sql(limit)
|
179
|
+
else super
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Deprecated in Rails 4.1. Backports functionality.
|
184
|
+
def add_column_options!(sql, options)
|
185
|
+
if options_include_default?(options)
|
186
|
+
sql << " DEFAULT #{quote(options[:default], options[:column])}"
|
187
|
+
end
|
188
|
+
# must explicitly check for :null to allow change_column to work on migrations
|
189
|
+
sql << ' NOT NULL' if options[:null] == false
|
190
|
+
end if ActiveRecord::VERSION::MAJOR > 3
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
195
|
+
def create_table_definition(*args)
|
196
|
+
TableDefinition.new(native_database_types, *args)
|
197
|
+
end
|
198
|
+
else
|
199
|
+
def table_definition
|
200
|
+
TableDefinition.new(self)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Map logical Rails types to Firebird-specific data types.
|
205
|
+
def integer_to_sql(limit)
|
206
|
+
return 'integer' if limit.nil?
|
207
|
+
case limit
|
208
|
+
when 1..2 then 'smallint'
|
209
|
+
when 3..4 then 'integer'
|
210
|
+
when 5..8 then 'bigint'
|
211
|
+
else
|
212
|
+
fail ActiveRecordError, "No integer type has byte size #{limit}. Use a NUMERIC with PRECISION 0 instead."
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def float_to_sql(limit)
|
217
|
+
if limit.nil? || limit <= 4
|
218
|
+
'float'
|
219
|
+
else
|
220
|
+
'double precision'
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Creates a domain for boolean fields as needed
|
225
|
+
def while_ensuring_boolean_domain(&block)
|
226
|
+
block.call
|
227
|
+
rescue ActiveRecord::StatementInvalid => e
|
228
|
+
raise unless e.message =~ /Specified domain or source column \w+ does not exist/
|
229
|
+
create_boolean_domain
|
230
|
+
block.call
|
231
|
+
end
|
232
|
+
|
233
|
+
def create_boolean_domain
|
234
|
+
sql = <<-end_sql
|
235
|
+
CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
|
236
|
+
CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
|
237
|
+
end_sql
|
238
|
+
execute(sql)
|
239
|
+
end
|
240
|
+
|
241
|
+
def sequence_exists?(sequence_name)
|
242
|
+
@connection.generator_names.include?(sequence_name)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|