activerecord-rdb-adapter 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/lib/active_record/connection_adapters/rdb/database_limits.rb +35 -0
- data/lib/active_record/connection_adapters/rdb/database_statements.rb +99 -0
- data/lib/active_record/connection_adapters/rdb/quoting.rb +147 -0
- data/lib/active_record/connection_adapters/rdb/schema_creation.rb +53 -0
- data/lib/active_record/connection_adapters/rdb/schema_dumper.rb +23 -0
- data/lib/active_record/connection_adapters/rdb/schema_statements.rb +425 -0
- data/lib/active_record/connection_adapters/rdb/table_definition.rb +28 -0
- data/lib/active_record/connection_adapters/rdb_adapter.rb +152 -0
- data/lib/active_record/connection_adapters/rdb_column.rb +69 -0
- data/lib/active_record/rdb_base.rb +33 -0
- data/lib/active_record/tasks/rdb_database_tasks.rb +82 -0
- data/lib/activerecord-rdb-adapter.rb +10 -0
- data/lib/arel/visitors/rdb_visitor.rb +117 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: eae56962a83ba7f51da156eb041ef6b9961e450d29088e2481b3085c4913d1b8
|
4
|
+
data.tar.gz: 38fcc781c4852ef2dd1897d0affebaac9006e038dbb90a67efe4ba9995a4e408
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1f68f8293992979c5f6168746cabed86d7c68290424aa566134db01e40cf3ec9097e53628c05bf71906197ebaeb4f6dec9271a062ba6e9d6b4b6050e00ebd064
|
7
|
+
data.tar.gz: c5dc9601b8205894ca3f668f25848643a69b79093b2069d87d9a60e2d80eb889f77e00b465e797ee765fea0d77b8c5eeb5746776017ca0a5d432c153f6b94890
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
module DatabaseLimits # :nodoc:
|
5
|
+
def table_alias_length
|
6
|
+
31
|
7
|
+
end
|
8
|
+
|
9
|
+
def column_name_length
|
10
|
+
31
|
11
|
+
end
|
12
|
+
|
13
|
+
def table_name_length
|
14
|
+
31
|
15
|
+
end
|
16
|
+
|
17
|
+
def index_name_length
|
18
|
+
31
|
19
|
+
end
|
20
|
+
|
21
|
+
def indexes_per_table
|
22
|
+
65_535
|
23
|
+
end
|
24
|
+
|
25
|
+
def in_clause_length
|
26
|
+
1_499
|
27
|
+
end
|
28
|
+
|
29
|
+
def sql_query_length
|
30
|
+
32_767
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
module DatabaseStatements # :nodoc:
|
5
|
+
def execute(sql, name = nil)
|
6
|
+
log(sql, name) do
|
7
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
8
|
+
@connection.query(sql)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def exec_query(sql, name = 'SQL', binds = [], prepare: false)
|
14
|
+
type_casted_binds = type_casted_binds(binds)
|
15
|
+
|
16
|
+
log(sql, name, binds, type_casted_binds) do
|
17
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
18
|
+
result = @connection.execute(sql, *type_casted_binds)
|
19
|
+
if result.is_a?(Fb::Cursor)
|
20
|
+
fields = result.fields.map(&:name)
|
21
|
+
rows = result.fetchall.map do |row|
|
22
|
+
row.map do |col|
|
23
|
+
col.encode('UTF-8', @connection.encoding)
|
24
|
+
rescue StandardError
|
25
|
+
col
|
26
|
+
end
|
27
|
+
end
|
28
|
+
result.close
|
29
|
+
ActiveRecord::Result.new(fields, rows)
|
30
|
+
else
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue StandardError => e
|
36
|
+
raise e.message.encode('UTF-8', @connection.encoding)
|
37
|
+
end
|
38
|
+
|
39
|
+
def explain(arel, binds = [])
|
40
|
+
to_sql(arel, binds)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Begins the transaction (and turns off auto-committing).
|
44
|
+
def begin_db_transaction
|
45
|
+
log('begin transaction', nil) do
|
46
|
+
begin_isolated_db_transaction(default_transaction_isolation)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Default isolation levels for transactions. This method exists
|
51
|
+
# in 4.0.2+, so it's here for backward compatibility with AR 3
|
52
|
+
def transaction_isolation_levels
|
53
|
+
{
|
54
|
+
read_committed: 'READ COMMITTED',
|
55
|
+
repeatable_read: 'REPEATABLE READ',
|
56
|
+
serializable: 'SERIALIZABLE'
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Allows providing the :transaction option to ActiveRecord::Base.transaction
|
61
|
+
# in 4.0.2+. Can accept verbatim isolation options like 'WAIT READ COMMITTED'
|
62
|
+
def begin_isolated_db_transaction(isolation)
|
63
|
+
@connection.transaction transaction_isolation_levels.fetch(isolation, isolation)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Commits the transaction (and turns on auto-committing).
|
67
|
+
def commit_db_transaction
|
68
|
+
log('commit transaction', nil) { @connection.commit }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
72
|
+
# done if the transaction block raises an exception or returns false.
|
73
|
+
def rollback_db_transaction
|
74
|
+
log('rollback transaction', nil) { @connection.rollback }
|
75
|
+
end
|
76
|
+
|
77
|
+
def default_sequence_name(table_name, _column = nil)
|
78
|
+
"#{table_name.to_s.tr('-', '_')[0, table_name_length - 4]}_seq"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set the sequence to the max value of the table's column.
|
82
|
+
def reset_sequence!(table, column, sequence = nil)
|
83
|
+
sequence ||= default_sequence_name(table, column)
|
84
|
+
max_id = select_value("select max(#{column}) from #{table}")
|
85
|
+
execute("alter sequence #{sequence} restart with #{max_id}")
|
86
|
+
end
|
87
|
+
|
88
|
+
# Uses the raw connection to get the next sequence value.
|
89
|
+
def next_sequence_value(sequence_name)
|
90
|
+
@connection.query("SELECT NEXT VALUE FOR #{sequence_name} FROM RDB$DATABASE")[0][0]
|
91
|
+
end
|
92
|
+
|
93
|
+
def last_inserted_id(_result)
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
module Quoting # :nodoc:
|
5
|
+
QUOTED_FALSE = "'false'".freeze
|
6
|
+
QUOTED_TRUE = "'true'".freeze
|
7
|
+
|
8
|
+
QUOTED_POSITION = '"POSITION"'.freeze
|
9
|
+
QUOTED_VALUE = '"VALUE"'.freeze
|
10
|
+
|
11
|
+
def quote_string(string) # :nodoc:
|
12
|
+
string.gsub(/'/, "''")
|
13
|
+
end
|
14
|
+
|
15
|
+
def quoted_date(time)
|
16
|
+
if time.is_a?(Time) || time.is_a?(DateTime)
|
17
|
+
time.localtime.strftime('%d.%m.%Y %H:%M:%S')
|
18
|
+
else
|
19
|
+
time.strftime('%d.%m.%Y')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def quote_column_name(column_name) # :nodoc:
|
24
|
+
column = column_name.dup.to_s
|
25
|
+
column.gsub!(/(?<=[^\"\w]|^)position(?=[^\"\w]|$)/i, QUOTED_POSITION)
|
26
|
+
column.gsub!(/(?<=[^\"\w]|^)value(?=[^\"\w]|$)/i, QUOTED_VALUE)
|
27
|
+
column.delete!('"')
|
28
|
+
column.upcase!
|
29
|
+
@connection.dialect == 1 ? column.to_s : %("#{column}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def quote_table_name_for_assignment(_table, attr)
|
33
|
+
quote_column_name(attr)
|
34
|
+
end
|
35
|
+
|
36
|
+
def unquoted_true
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def quoted_true # :nodoc:
|
41
|
+
QUOTED_TRUE
|
42
|
+
end
|
43
|
+
|
44
|
+
def unquoted_false
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def quoted_false # :nodoc:
|
49
|
+
QUOTED_FALSE
|
50
|
+
end
|
51
|
+
|
52
|
+
def type_cast_from_column(column, value) # :nodoc:
|
53
|
+
if column
|
54
|
+
type = column.type || lookup_cast_type_from_column(column)
|
55
|
+
type.serialize(value)
|
56
|
+
else
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def lookup_cast_type_from_column(column) # :nodoc:
|
62
|
+
type = column.try(:sql_type) || column.try(:type)
|
63
|
+
lookup_cast_type(type)
|
64
|
+
end
|
65
|
+
|
66
|
+
def type_casted_binds(binds) # :nodoc:
|
67
|
+
if binds.first.is_a?(Array)
|
68
|
+
binds.map { |column, value| type_cast(value, column) }
|
69
|
+
else
|
70
|
+
binds.map { |attr| type_cast(attr.value_for_database, attr) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def id_value_for_database(value)
|
77
|
+
if primary_key = value.class.primary_key
|
78
|
+
value.instance_variable_get(:@attributes)[primary_key].value_for_database
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def _quote(value)
|
83
|
+
case value
|
84
|
+
when Time, DateTime
|
85
|
+
"'#{value.strftime('%d.%m.%Y %H:%M')}'"
|
86
|
+
when Date
|
87
|
+
"'#{value.strftime('%d.%m.%Y')}'"
|
88
|
+
else
|
89
|
+
super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def _type_cast(value)
|
94
|
+
case value
|
95
|
+
when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
|
96
|
+
value.to_s
|
97
|
+
when Array
|
98
|
+
value.to_yaml
|
99
|
+
when Hash then
|
100
|
+
encode_hash(value)
|
101
|
+
when true then
|
102
|
+
unquoted_true
|
103
|
+
when false then
|
104
|
+
unquoted_false
|
105
|
+
# BigDecimals need to be put in a non-normalized form and quoted.
|
106
|
+
when BigDecimal then
|
107
|
+
value.to_s('F')
|
108
|
+
when Type::Time::Value then
|
109
|
+
quoted_time(value)
|
110
|
+
when Date, Time, DateTime then
|
111
|
+
quoted_date(value)
|
112
|
+
when *types_which_need_no_typecasting
|
113
|
+
value
|
114
|
+
else
|
115
|
+
raise TypeError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def rdb_to_ar_case(column_name)
|
120
|
+
/[[:lower:]]/.match?(column_name) ? column_name : column_name.downcase
|
121
|
+
end
|
122
|
+
|
123
|
+
def ar_to_rdb_case(column_name)
|
124
|
+
/[[:upper:]]/.match?(column_name) ? column_name : column_name.upcase
|
125
|
+
end
|
126
|
+
|
127
|
+
def encode_hash(value)
|
128
|
+
if value.is_a?(Hash)
|
129
|
+
value.to_yaml
|
130
|
+
else
|
131
|
+
value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
if defined? Encoding
|
136
|
+
def decode(str)
|
137
|
+
Base64.decode64(str).force_encoding(@connection.encoding)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
def decode(str)
|
141
|
+
Base64.decode64(str)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def visit_ColumnDefinition(o)
|
9
|
+
o.sql_type = type_to_sql(o.type, o.options)
|
10
|
+
column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
|
11
|
+
add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
|
12
|
+
column_sql
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_column_options!(sql, options)
|
16
|
+
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
|
17
|
+
# must explicitly check for :null to allow change_column to work on migrations
|
18
|
+
if !options[:null]
|
19
|
+
sql << " NOT NULL"
|
20
|
+
end
|
21
|
+
if options[:auto_increment]
|
22
|
+
sql << " AUTO_INCREMENT"
|
23
|
+
end
|
24
|
+
if options[:primary_key]
|
25
|
+
sql << " PRIMARY KEY"
|
26
|
+
end
|
27
|
+
sql
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_TableDefinition(o)
|
31
|
+
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
|
32
|
+
|
33
|
+
statements = o.columns.map(&method(:accept))
|
34
|
+
statements << accept(o.primary_keys) if o.primary_keys
|
35
|
+
|
36
|
+
if supports_indexes_in_create?
|
37
|
+
statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
|
38
|
+
end
|
39
|
+
|
40
|
+
if supports_foreign_keys_in_create?
|
41
|
+
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
|
42
|
+
end
|
43
|
+
|
44
|
+
create_sql << "(#{statements.join(', ')})" if statements.present?
|
45
|
+
add_table_options!(create_sql, table_options(o))
|
46
|
+
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
|
47
|
+
create_sql
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
|
5
|
+
private
|
6
|
+
|
7
|
+
def column_spec_for_primary_key(column)
|
8
|
+
spec = super
|
9
|
+
spec.delete(:auto_increment) if column.type == :integer && column.auto_increment?
|
10
|
+
spec
|
11
|
+
end
|
12
|
+
|
13
|
+
def schema_type(column)
|
14
|
+
if column.bigint?
|
15
|
+
:bigint
|
16
|
+
else
|
17
|
+
column.type.type
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,425 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
module SchemaStatements # :nodoc:
|
5
|
+
methods_to_commit = %i[add_column
|
6
|
+
create_table
|
7
|
+
rename_column
|
8
|
+
remove_column
|
9
|
+
change_column
|
10
|
+
change_column_default
|
11
|
+
change_column_null
|
12
|
+
remove_index
|
13
|
+
remove_index!
|
14
|
+
drop_table
|
15
|
+
create_sequence
|
16
|
+
drop_sequence
|
17
|
+
drop_trigger]
|
18
|
+
|
19
|
+
def tables(_name = nil)
|
20
|
+
@connection.table_names
|
21
|
+
end
|
22
|
+
|
23
|
+
def views
|
24
|
+
@connection.view_names
|
25
|
+
end
|
26
|
+
|
27
|
+
def indexes(table_name, _name = nil)
|
28
|
+
@connection.indexes.values.map do |ix|
|
29
|
+
IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns) if ix.table_name == table_name.to_s && ix.index_name !~ /^rdb\$/
|
30
|
+
end.compact
|
31
|
+
end
|
32
|
+
|
33
|
+
def index_name_exists?(table_name, index_name)
|
34
|
+
index_name = index_name.to_s.upcase
|
35
|
+
indexes(table_name).detect { |i| i.name.upcase == index_name }
|
36
|
+
end
|
37
|
+
|
38
|
+
def columns(table_name, _name = nil)
|
39
|
+
@col_definitions ||= {}
|
40
|
+
@col_definitions[table_name] = column_definitions(table_name).map do |field|
|
41
|
+
sql_type_metadata = column_type_for(field)
|
42
|
+
rdb_opt = { domain: field[:domain], sub_type: field[:sql_subtype] }
|
43
|
+
RdbColumn.new(field[:name], field[:default], sql_type_metadata, field[:nullable], table_name, rdb_opt)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_table(name, options = {}) # :nodoc:
|
48
|
+
raise ActiveRecordError, 'Firebird does not support temporary tables' if options.key? :temporary
|
49
|
+
|
50
|
+
raise ActiveRecordError, 'Firebird does not support creating tables with a select' if options.key? :as
|
51
|
+
|
52
|
+
drop_table name, if_exists: true if options.key? :force
|
53
|
+
|
54
|
+
needs_sequence = options[:id]
|
55
|
+
|
56
|
+
super name, options do |table_def|
|
57
|
+
yield table_def if block_given?
|
58
|
+
needs_sequence ||= table_def.needs_sequence
|
59
|
+
end
|
60
|
+
|
61
|
+
return if options[:sequence] == false || !needs_sequence
|
62
|
+
create_sequence(options[:sequence] || default_sequence_name(name))
|
63
|
+
trg_sql = <<-END_SQL
|
64
|
+
CREATE TRIGGER N$#{name.upcase} FOR #{name.upcase}
|
65
|
+
ACTIVE BEFORE INSERT
|
66
|
+
AS
|
67
|
+
declare variable gen_val bigint;
|
68
|
+
BEGIN
|
69
|
+
if (new.ID is null) then
|
70
|
+
new.ID = next value for #{options[:sequence] || default_sequence_name(name)};
|
71
|
+
else begin
|
72
|
+
gen_val = gen_id(#{options[:sequence] || default_sequence_name(name)}, 1);
|
73
|
+
if (new.ID > gen_val) then
|
74
|
+
gen_val = gen_id(#{options[:sequence] || default_sequence_name(name)}, new.ID - gen_val);
|
75
|
+
end
|
76
|
+
END
|
77
|
+
END_SQL
|
78
|
+
execute(trg_sql)
|
79
|
+
end
|
80
|
+
|
81
|
+
def drop_table(name, options = {}) # :nodoc:
|
82
|
+
drop_sql = "DROP TABLE #{quote_table_name(name)}"
|
83
|
+
if options[:if_exists]
|
84
|
+
drop = !execute(squish_sql(<<-END_SQL))
|
85
|
+
select 1 from rdb$relations where rdb$relation_name = #{quote_table_name(name).tr('"', '\'')}
|
86
|
+
END_SQL
|
87
|
+
.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
trigger_name = "N$#{name.upcase}"
|
91
|
+
drop_trigger(trigger_name) if trigger_exists?(trigger_name)
|
92
|
+
|
93
|
+
sequence_name = options[:sequence] || default_sequence_name(name)
|
94
|
+
drop_sequence(sequence_name) if sequence_exists?(sequence_name)
|
95
|
+
|
96
|
+
execute(drop_sql) if drop
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_sequence(sequence_name)
|
100
|
+
execute("CREATE SEQUENCE #{sequence_name}")
|
101
|
+
rescue StandardError
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def drop_sequence(sequence_name)
|
106
|
+
execute("DROP SEQUENCE #{sequence_name}")
|
107
|
+
rescue StandardError
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
def drop_trigger(trigger_name)
|
112
|
+
execute("DROP TRIGGER #{trigger_name}")
|
113
|
+
rescue StandardError
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def trigger_exists?(trigger_name)
|
118
|
+
!execute(squish_sql(<<-END_SQL))
|
119
|
+
select 1
|
120
|
+
from rdb$triggers
|
121
|
+
where rdb$trigger_name = '#{trigger_name}'
|
122
|
+
END_SQL
|
123
|
+
.empty?
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_column(table_name, column_name, type, options = {})
|
127
|
+
super
|
128
|
+
|
129
|
+
create_sequence(options[:sequence] || default_sequence_name(table_name)) if type == :primary_key && options[:sequence] != false
|
130
|
+
|
131
|
+
return unless options[:position]
|
132
|
+
# position is 1-based but add 1 to skip id column
|
133
|
+
execute(squish_sql(<<-end_sql))
|
134
|
+
ALTER TABLE #{quote_table_name(table_name)}
|
135
|
+
ALTER COLUMN #{quote_column_name(column_name)}
|
136
|
+
POSITION #{options[:position] + 1}
|
137
|
+
end_sql
|
138
|
+
end
|
139
|
+
|
140
|
+
def remove_column(table_name, column_name, type = nil, options = {})
|
141
|
+
indexes(table_name).each do |i|
|
142
|
+
remove_index! i.table, i.name if i.columns.any? { |c| c == column_name.to_s }
|
143
|
+
end
|
144
|
+
|
145
|
+
column_exist = !execute(squish_sql(<<-END_SQL))
|
146
|
+
select 1 from RDB$RELATION_FIELDS rf
|
147
|
+
where lower(rf.RDB$RELATION_NAME) = '#{table_name.downcase}' and lower(rf.RDB$FIELD_NAME) = '#{column_name.downcase}'
|
148
|
+
END_SQL
|
149
|
+
.empty?
|
150
|
+
super if column_exist
|
151
|
+
end
|
152
|
+
|
153
|
+
def remove_column_for_alter(_table_name, column_name, _type = nil, _options = {})
|
154
|
+
"DROP #{quote_column_name(column_name)}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def change_column(table_name, column_name, type, options = {})
|
158
|
+
type_sql = type_to_sql(type, *options.values_at(:limit, :precision, :scale))
|
159
|
+
|
160
|
+
if %i[text string].include?(type)
|
161
|
+
copy_column = 'c_temp'
|
162
|
+
add_column table_name, copy_column, type, options
|
163
|
+
execute(squish_sql(<<-END_SQL))
|
164
|
+
UPDATE #{table_name} SET #{copy_column.quote_column_name} = #{column_name.to_s.quote_column_name};
|
165
|
+
END_SQL
|
166
|
+
remove_column table_name, column_name
|
167
|
+
rename_column table_name, copy_column, column_name
|
168
|
+
else
|
169
|
+
execute(squish_sql(<<-END_SQL))
|
170
|
+
ALTER TABLE #{quote_table_name(table_name)}
|
171
|
+
ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_sql}
|
172
|
+
END_SQL
|
173
|
+
end
|
174
|
+
change_column_null(table_name, column_name, !!options[:null]) if options.key?(:null)
|
175
|
+
change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
|
176
|
+
end
|
177
|
+
|
178
|
+
def change_column_default(table_name, column_name, default)
|
179
|
+
execute(squish_sql(<<-END_SQL))
|
180
|
+
ALTER TABLE #{quote_table_name(table_name)}
|
181
|
+
ALTER #{quote_column_name(column_name)}
|
182
|
+
SET DEFAULT #{quote(default)}
|
183
|
+
END_SQL
|
184
|
+
end
|
185
|
+
|
186
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
187
|
+
change_column_default(table_name, column_name, default) if default
|
188
|
+
|
189
|
+
db_column = columns(table_name).find { |c| c.name == column_name.to_s }
|
190
|
+
options = { null: null }
|
191
|
+
options[:default] = db_column.default if !default && db_column.default
|
192
|
+
options[:default] = default if default
|
193
|
+
ar_type = db_column.type
|
194
|
+
type = type_to_sql(ar_type.type, ar_type.limit, ar_type.precision, ar_type.scale)
|
195
|
+
|
196
|
+
copy_column = 'c_temp'
|
197
|
+
add_column table_name, copy_column, type, options
|
198
|
+
execute(squish_sql(<<-END_SQL))
|
199
|
+
UPDATE #{table_name} SET #{copy_column.quote_column_name} = #{column_name.to_s.quote_column_name};
|
200
|
+
END_SQL
|
201
|
+
remove_column table_name, column_name
|
202
|
+
rename_column table_name, copy_column, column_name
|
203
|
+
end
|
204
|
+
|
205
|
+
def rename_column(table_name, column_name, new_column_name)
|
206
|
+
execute(squish_sql(<<-END_SQL))
|
207
|
+
ALTER TABLE #{quote_table_name(table_name)}
|
208
|
+
ALTER #{quote_column_name(column_name)}
|
209
|
+
TO #{quote_column_name(new_column_name)}
|
210
|
+
END_SQL
|
211
|
+
|
212
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
213
|
+
end
|
214
|
+
|
215
|
+
def remove_index!(_table_name, index_name)
|
216
|
+
execute "DROP INDEX #{quote_column_name(index_name)}"
|
217
|
+
end
|
218
|
+
|
219
|
+
def remove_index(table_name, options = {})
|
220
|
+
index_name = index_name(table_name, options)
|
221
|
+
execute "DROP INDEX #{quote_column_name(index_name)}"
|
222
|
+
end
|
223
|
+
|
224
|
+
def index_name(table_name, options) #:nodoc:
|
225
|
+
if options.respond_to?(:keys) # legacy support
|
226
|
+
if options[:column]
|
227
|
+
index_name = "#{table_name}_#{Array.wrap(options[:column]) * '_'}"
|
228
|
+
if index_name.length > 31
|
229
|
+
"IDX_#{Digest::SHA256.hexdigest(index_name)[0..22]}"
|
230
|
+
else
|
231
|
+
index_name
|
232
|
+
end
|
233
|
+
elsif options[:name]
|
234
|
+
options[:name]
|
235
|
+
else
|
236
|
+
raise ArgumentError 'You must specify the index name'
|
237
|
+
end
|
238
|
+
else
|
239
|
+
index_name(table_name, column: options)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def index_exists?(table_name, column_name, options = {})
|
244
|
+
column_names = Array(column_name).map(&:to_s)
|
245
|
+
checks = []
|
246
|
+
checks << lambda { |i| i.columns == column_names }
|
247
|
+
checks << lambda(&:unique) if options[:unique]
|
248
|
+
checks << lambda { |i| i.name.upcase == options[:name].to_s.upcase } if options[:name]
|
249
|
+
|
250
|
+
indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
|
251
|
+
end
|
252
|
+
|
253
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil, **args)
|
254
|
+
if !args.nil? && !args.empty?
|
255
|
+
limit = args[:limit] if limit.nil?
|
256
|
+
precision = args[:precision] if precision.nil?
|
257
|
+
scale = args[:scale] if scale.nil?
|
258
|
+
end
|
259
|
+
case type
|
260
|
+
when :integer
|
261
|
+
integer_to_sql(limit)
|
262
|
+
when :float
|
263
|
+
float_to_sql(limit)
|
264
|
+
when :text
|
265
|
+
text_to_sql(limit)
|
266
|
+
# when :blob
|
267
|
+
# binary_to_sql(limit)
|
268
|
+
when :string
|
269
|
+
string_to_sql(limit)
|
270
|
+
else
|
271
|
+
type = type.to_sym if type
|
272
|
+
if native = native_database_types[type]
|
273
|
+
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
|
274
|
+
|
275
|
+
if type == :decimal # ignore limit, use precision and scale
|
276
|
+
scale ||= native[:scale]
|
277
|
+
|
278
|
+
if precision ||= native[:precision]
|
279
|
+
column_type_sql << if scale
|
280
|
+
"(#{precision},#{scale})"
|
281
|
+
else
|
282
|
+
"(#{precision})"
|
283
|
+
end
|
284
|
+
elsif scale
|
285
|
+
raise ArgumentError, 'Error adding decimal column: precision cannot be empty if scale is specified'
|
286
|
+
end
|
287
|
+
|
288
|
+
elsif %i[datetime timestamp time interval].include?(type) && precision ||= native[:precision]
|
289
|
+
if (0..6) === precision
|
290
|
+
column_type_sql << "(#{precision})"
|
291
|
+
else
|
292
|
+
raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
|
293
|
+
end
|
294
|
+
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
|
295
|
+
column_type_sql << "(#{limit})"
|
296
|
+
end
|
297
|
+
|
298
|
+
column_type_sql
|
299
|
+
else
|
300
|
+
type.to_s
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def primary_key(table_name)
|
306
|
+
row = @connection.query(<<-END_SQL)
|
307
|
+
SELECT s.rdb$field_name
|
308
|
+
FROM rdb$indices i
|
309
|
+
JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
|
310
|
+
LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
|
311
|
+
WHERE i.rdb$relation_name = '#{ar_to_rdb_case(table_name)}'
|
312
|
+
AND c.rdb$constraint_type = 'PRIMARY KEY';
|
313
|
+
END_SQL
|
314
|
+
|
315
|
+
row.first && rdb_to_ar_case(row.first[0].rstrip)
|
316
|
+
end
|
317
|
+
|
318
|
+
def native_database_types
|
319
|
+
@native_database_types ||= initialize_native_database_types.freeze
|
320
|
+
end
|
321
|
+
|
322
|
+
def create_schema_dumper(options)
|
323
|
+
Rdb::SchemaDumper.create(self, options)
|
324
|
+
end
|
325
|
+
|
326
|
+
private
|
327
|
+
|
328
|
+
def column_definitions(table_name)
|
329
|
+
@connection.columns(table_name)
|
330
|
+
end
|
331
|
+
|
332
|
+
def new_column_from_field(table_name, field)
|
333
|
+
type_metadata = fetch_type_metadata(field['sql_type'])
|
334
|
+
ActiveRecord::ConnectionAdapters::Column.new(field['name'], field['default'], type_metadata, field['nullable'], table_name)
|
335
|
+
end
|
336
|
+
|
337
|
+
def column_type_for(field)
|
338
|
+
sql_type = RdbColumn.sql_type_for(field)
|
339
|
+
type = lookup_cast_type(sql_type)
|
340
|
+
{ type: type, sql_type: type.type }
|
341
|
+
end
|
342
|
+
|
343
|
+
def integer_to_sql(limit)
|
344
|
+
return 'integer' if limit.nil?
|
345
|
+
case limit
|
346
|
+
when 1..2 then
|
347
|
+
'smallint'
|
348
|
+
when 3..4 then
|
349
|
+
'integer'
|
350
|
+
when 5..8 then
|
351
|
+
'bigint'
|
352
|
+
else
|
353
|
+
raise ActiveRecordError "No integer type has byte size #{limit}. " \
|
354
|
+
'Use a NUMERIC with PRECISION 0 instead.'
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def float_to_sql(limit)
|
359
|
+
limit.nil? || limit <= 4 ? 'float' : 'double precision'
|
360
|
+
end
|
361
|
+
|
362
|
+
def text_to_sql(limit)
|
363
|
+
if limit && limit > 0
|
364
|
+
"VARCHAR(#{limit})"
|
365
|
+
else
|
366
|
+
'BLOB SUB_TYPE TEXT'
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def string_to_sql(limit)
|
371
|
+
if limit && limit > 0
|
372
|
+
"VARCHAR(#{limit})"
|
373
|
+
else
|
374
|
+
'VARCHAR(150)'
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def initialize_native_database_types
|
379
|
+
{ primary_key: 'integer not null primary key',
|
380
|
+
string: { name: 'varchar', limit: 255 },
|
381
|
+
text: { name: 'blob sub_type text' },
|
382
|
+
integer: { name: 'integer' },
|
383
|
+
bigint: { name: 'bigint' },
|
384
|
+
float: { name: 'float' },
|
385
|
+
decimal: { name: 'decimal' },
|
386
|
+
datetime: { name: 'timestamp' },
|
387
|
+
timestamp: { name: 'timestamp' },
|
388
|
+
time: { name: 'time' },
|
389
|
+
date: { name: 'date' },
|
390
|
+
binary: { name: 'blob' },
|
391
|
+
boolean: { name: 'boolean' } }
|
392
|
+
end
|
393
|
+
|
394
|
+
def sequence_exists?(sequence_name)
|
395
|
+
@connection.generator_names.include?(sequence_name)
|
396
|
+
end
|
397
|
+
|
398
|
+
def create_table_definition(*args)
|
399
|
+
Rdb::TableDefinition.new(*args)
|
400
|
+
end
|
401
|
+
|
402
|
+
def squish_sql(sql)
|
403
|
+
sql.strip.gsub(/\s+/, ' ')
|
404
|
+
end
|
405
|
+
|
406
|
+
class << self
|
407
|
+
def after(*names)
|
408
|
+
names.flatten.each do |name|
|
409
|
+
m = ActiveRecord::ConnectionAdapters::Rdb::SchemaStatements.instance_method(name)
|
410
|
+
define_method(name) do |*args, &block|
|
411
|
+
m.bind(self).call(*args, &block)
|
412
|
+
yield
|
413
|
+
commit_db_transaction
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
after(methods_to_commit) do
|
420
|
+
puts 'Commiting transaction'
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Rdb
|
4
|
+
module ColumnMethods # :nodoc:
|
5
|
+
|
6
|
+
attr_accessor :needs_sequence
|
7
|
+
|
8
|
+
def primary_key(name, type = :primary_key, **options)
|
9
|
+
self.needs_sequence = true
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition # :nodoc:
|
16
|
+
include ColumnMethods
|
17
|
+
|
18
|
+
def new_column_definition(name, type, **options)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Table < ActiveRecord::ConnectionAdapters::Table # :nodoc:
|
24
|
+
include ColumnMethods
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'fb'
|
2
|
+
require 'base64'
|
3
|
+
require 'arel'
|
4
|
+
require 'arel/visitors/rdb_visitor'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
require 'active_record/base'
|
8
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
9
|
+
require 'active_record/connection_adapters/rdb/database_statements'
|
10
|
+
require 'active_record/connection_adapters/rdb/database_limits'
|
11
|
+
require 'active_record/connection_adapters/rdb/schema_creation'
|
12
|
+
require 'active_record/connection_adapters/rdb/schema_dumper'
|
13
|
+
require 'active_record/connection_adapters/rdb/schema_statements'
|
14
|
+
require 'active_record/connection_adapters/rdb/quoting'
|
15
|
+
require 'active_record/connection_adapters/rdb/table_definition'
|
16
|
+
require 'active_record/connection_adapters/rdb_column'
|
17
|
+
require 'active_record/rdb_base'
|
18
|
+
|
19
|
+
module ActiveRecord
|
20
|
+
module ConnectionAdapters
|
21
|
+
class RdbAdapter < AbstractAdapter # :nodoc:
|
22
|
+
include Rdb::DatabaseLimits
|
23
|
+
include Rdb::DatabaseStatements
|
24
|
+
include Rdb::Quoting
|
25
|
+
include Rdb::SchemaStatements
|
26
|
+
|
27
|
+
@@default_transaction_isolation = :read_committed
|
28
|
+
cattr_accessor :default_transaction_isolation
|
29
|
+
|
30
|
+
ADAPTER_NAME = 'rdb'.freeze
|
31
|
+
|
32
|
+
def initialize(connection, logger = nil, config = {})
|
33
|
+
super(connection, logger, config)
|
34
|
+
# Our Responsibility
|
35
|
+
@config = config
|
36
|
+
@visitor = Arel::Visitors::Rdb.new self
|
37
|
+
end
|
38
|
+
|
39
|
+
def arel_visitor
|
40
|
+
Arel::Visitors::Rdb.new self
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid_type?(type)
|
44
|
+
!native_database_types[type].nil? || !native_database_types[type.type].nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def adapter_name
|
48
|
+
ADAPTER_NAME
|
49
|
+
end
|
50
|
+
|
51
|
+
def schema_creation
|
52
|
+
Rdb::SchemaCreation.new self
|
53
|
+
end
|
54
|
+
|
55
|
+
def supports_migrations?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def supports_primary_key?
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
def supports_count_distinct?
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def supports_ddl_transactions?
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def supports_transaction_isolation?
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def supports_savepoints?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def prefetch_primary_key?(_table_name = nil)
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def ids_in_list_limit
|
84
|
+
1499
|
85
|
+
end
|
86
|
+
|
87
|
+
def active?
|
88
|
+
return false unless @connection.open?
|
89
|
+
# return true if @connection.transaction_started
|
90
|
+
@connection.query('SELECT 1 FROM RDB$DATABASE')
|
91
|
+
true
|
92
|
+
rescue StandardError
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
def reconnect!
|
97
|
+
disconnect!
|
98
|
+
@connection = ::Fb::Database.connect(@config)
|
99
|
+
end
|
100
|
+
|
101
|
+
def disconnect!
|
102
|
+
super
|
103
|
+
begin
|
104
|
+
@connection.close
|
105
|
+
rescue StandardError
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def reset!
|
111
|
+
reconnect!
|
112
|
+
end
|
113
|
+
|
114
|
+
def requires_reloading?
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
def create_savepoint(name = current_savepoint_name)
|
119
|
+
execute("SAVEPOINT #{name}")
|
120
|
+
end
|
121
|
+
|
122
|
+
def rollback_to_savepoint(name = current_savepoint_name)
|
123
|
+
execute("ROLLBACK TO SAVEPOINT #{name}")
|
124
|
+
end
|
125
|
+
|
126
|
+
def release_savepoint(name = current_savepoint_name)
|
127
|
+
execute("RELEASE SAVEPOINT #{name}")
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
def initialize_type_map(map)
|
133
|
+
super
|
134
|
+
map.register_type(/timestamp/i, Type::DateTime.new)
|
135
|
+
map.alias_type(/blob sub_type text/i, 'text')
|
136
|
+
end
|
137
|
+
|
138
|
+
def translate_exception(e, message)
|
139
|
+
case e.message
|
140
|
+
when /violation of FOREIGN KEY constraint/
|
141
|
+
ActiveRecord::InvalidForeignKey.new(message)
|
142
|
+
when /violation of PRIMARY or UNIQUE KEY constraint/, /attempt to store duplicate value/
|
143
|
+
ActiveRecord::RecordNotUnique.new(message)
|
144
|
+
when /This operation is not defined for system tables/
|
145
|
+
ActiveRecord::ActiveRecordError.new(message)
|
146
|
+
else
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class RdbColumn < Column # :nodoc:
|
4
|
+
class << self
|
5
|
+
def sql_type_for(field)
|
6
|
+
sql_type = field[:sql_type]
|
7
|
+
sub_type = field[:sql_subtype]
|
8
|
+
|
9
|
+
sql_type << case sql_type
|
10
|
+
when /(numeric|decimal)/i
|
11
|
+
"(#{field[:precision]},#{field[:scale].abs})"
|
12
|
+
when /(int|float|double|char|varchar|bigint)/i
|
13
|
+
"(#{field[:length]})"
|
14
|
+
else
|
15
|
+
''
|
16
|
+
end
|
17
|
+
|
18
|
+
sql_type << ' sub_type text' if /blob/i.match?(sql_type) && sub_type == 1
|
19
|
+
sql_type
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :sub_type, :domain
|
24
|
+
|
25
|
+
def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, rdb_options = {})
|
26
|
+
@domain, @sub_type = rdb_options.values_at(:domain, :sub_type)
|
27
|
+
name = name.dup
|
28
|
+
name.downcase!
|
29
|
+
super(name, parse_default(default), sql_type_metadata, null, table_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def sql_type
|
33
|
+
@sql_type_metadata[:sql_type]
|
34
|
+
end
|
35
|
+
|
36
|
+
def type
|
37
|
+
@sql_type_metadata[:type]
|
38
|
+
end
|
39
|
+
|
40
|
+
def precision
|
41
|
+
@sql_type_metadata[:precision]
|
42
|
+
end
|
43
|
+
|
44
|
+
def scale
|
45
|
+
@sql_type_metadata[:scale]
|
46
|
+
end
|
47
|
+
|
48
|
+
def limit
|
49
|
+
@sql_type_metadata[:limit]
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def parse_default(default)
|
55
|
+
return if default.nil? || /null/i.match?(default)
|
56
|
+
d = default.dup
|
57
|
+
d.gsub!(/^\s*DEFAULT\s+/i, '')
|
58
|
+
d.gsub!(/(^'|'$)/, '')
|
59
|
+
d
|
60
|
+
end
|
61
|
+
|
62
|
+
def simplified_type(field_type)
|
63
|
+
return :datetime if /timestamp/i.match?(field_type)
|
64
|
+
return :text if /blob sub_type text/i.match?(field_type)
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionHandling # :nodoc:
|
3
|
+
def rdb_connection(config)
|
4
|
+
require 'fb'
|
5
|
+
config = rdb_connection_config(config)
|
6
|
+
db = ::Fb::Database.new(config)
|
7
|
+
begin
|
8
|
+
connection = db.connect
|
9
|
+
rescue StandardError
|
10
|
+
unless config[:create]
|
11
|
+
require 'pp'
|
12
|
+
pp config
|
13
|
+
raise ConnectionNotEstablished, 'No Firebird connections established.'
|
14
|
+
end
|
15
|
+
connection = db.create.connect
|
16
|
+
end
|
17
|
+
ConnectionAdapters::RdbAdapter.new(connection, logger, config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def rdb_connection_config(config)
|
21
|
+
config = config.symbolize_keys.dup.reverse_merge(downcase_names: true)
|
22
|
+
port = config[:port] || 3050
|
23
|
+
raise ArgumentError, 'No database specified. Missing argument: database.' unless config[:database]
|
24
|
+
config[:database] = File.expand_path(config[:database], defined?(Rails) && Rails.root) if config[:host].nil? || /localhost/i.match?(config[:host])
|
25
|
+
config[:database] = "#{config[:host]}/#{port}:#{config[:database]}" if config[:host]
|
26
|
+
# config[:charset] = config[:charset].gsub(/-/, '') if config[:charset]
|
27
|
+
# config[:encoding] = config[:encoding].gsub(/-/, '') if config[:encoding]
|
28
|
+
config[:page_size] = 8192 unless config[:page_size]
|
29
|
+
config[:readonly_selects] = true unless config[:readonly_selects].present?
|
30
|
+
config
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'active_record/tasks/database_tasks'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Tasks
|
5
|
+
class RdbDatabaseTasks # :nodoc:
|
6
|
+
delegate :rdb_connection_config, :establish_connection, to: ::ActiveRecord::Base
|
7
|
+
|
8
|
+
def initialize(configuration, root = ::ActiveRecord::Tasks::DatabaseTasks.root)
|
9
|
+
@root = root
|
10
|
+
@configuration = rdb_connection_config(configuration)
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
rdb_database.create
|
15
|
+
establish_connection configuration
|
16
|
+
rescue ::Fb::Error => e
|
17
|
+
raise unless e.message.include?('database or file exists')
|
18
|
+
raise DatabaseAlreadyExists
|
19
|
+
end
|
20
|
+
|
21
|
+
def drop
|
22
|
+
rdb_database.drop
|
23
|
+
rescue ::Fb::Error => e
|
24
|
+
raise ::ActiveRecord::ConnectionNotEstablished, e.message
|
25
|
+
end
|
26
|
+
|
27
|
+
def purge
|
28
|
+
begin
|
29
|
+
drop
|
30
|
+
rescue StandardError
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
create
|
34
|
+
end
|
35
|
+
|
36
|
+
def structure_dump(filename)
|
37
|
+
isql :extract, output: filename
|
38
|
+
end
|
39
|
+
|
40
|
+
def structure_load(filename)
|
41
|
+
isql input: filename
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def rdb_database
|
47
|
+
::Fb::Database.new(configuration)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Executes isql commands to load/dump the schema.
|
51
|
+
# The generated command might look like this:
|
52
|
+
# isql db/development.fdb -user SYSDBA -password masterkey -extract
|
53
|
+
def isql(*args)
|
54
|
+
opts = args.extract_options!
|
55
|
+
user, pass = configuration.values_at(:username, :password)
|
56
|
+
user ||= configuration[:user]
|
57
|
+
opts.reverse_merge!(user: user, password: pass)
|
58
|
+
cmd = [isql_executable, configuration[:database]]
|
59
|
+
cmd += opts.map { |name, val| "-#{name} #{val}" }
|
60
|
+
cmd += args.map { |flag| "-#{flag}" }
|
61
|
+
cmd = cmd.join(' ')
|
62
|
+
raise "Error running: #{cmd}" unless Kernel.system(cmd)
|
63
|
+
end
|
64
|
+
|
65
|
+
def isql_create(*_args)
|
66
|
+
"#{isql_executable} -input "
|
67
|
+
end
|
68
|
+
|
69
|
+
# Finds the isql command line utility from the PATH
|
70
|
+
# Many linux distros call this program isql-fb, instead of isql
|
71
|
+
def isql_executable
|
72
|
+
require 'mkmf'
|
73
|
+
exe = %w[isql-fb isql].detect(&method(:find_executable0))
|
74
|
+
exe || abort('Unable to find isql or isql-fb in your $PATH')
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :configuration
|
78
|
+
|
79
|
+
attr_reader :root
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'active_record/connection_adapters/rdb_adapter'
|
2
|
+
|
3
|
+
if defined?(::Rails::Railtie) && ::ActiveRecord::VERSION::MAJOR > 3
|
4
|
+
class Railtie < ::Rails::Railtie # :nodoc:
|
5
|
+
rake_tasks do
|
6
|
+
load 'active_record/tasks/rdb_database_tasks.rb'
|
7
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(/rdb/, ActiveRecord::Tasks::RdbDatabaseTasks)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Arel
|
2
|
+
module Visitors
|
3
|
+
class Rdb < Arel::Visitors::ToSql # :nodoc
|
4
|
+
|
5
|
+
def preparable
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def visit_Arel_Nodes_SelectStatement o, collector
|
12
|
+
collector << "SELECT "
|
13
|
+
collector = visit o.offset, collector if o.offset && !o.limit
|
14
|
+
|
15
|
+
collector = o.cores.inject(collector) {|c, x|
|
16
|
+
visit_Arel_Nodes_SelectCore(x, c)
|
17
|
+
}
|
18
|
+
|
19
|
+
unless o.orders.empty?
|
20
|
+
collector << ORDER_BY
|
21
|
+
len = o.orders.length - 1
|
22
|
+
o.orders.each_with_index {|x, i|
|
23
|
+
collector = visit(x, collector)
|
24
|
+
collector << COMMA unless len == i
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
if o.limit && o.offset
|
29
|
+
collector = limit_with_rows o, collector
|
30
|
+
elsif o.limit && !o.offset
|
31
|
+
collector = visit o.limit, collector
|
32
|
+
end
|
33
|
+
|
34
|
+
maybe_visit o.lock, collector
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_Arel_Nodes_SelectCore o, collector
|
38
|
+
if o.set_quantifier
|
39
|
+
collector = visit o.set_quantifier, collector
|
40
|
+
collector << SPACE
|
41
|
+
end
|
42
|
+
|
43
|
+
unless o.projections.empty?
|
44
|
+
len = o.projections.length - 1
|
45
|
+
o.projections.each_with_index do |x, i|
|
46
|
+
collector = visit(x, collector)
|
47
|
+
collector << COMMA unless len == i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if o.source && !o.source.empty?
|
52
|
+
collector << " FROM "
|
53
|
+
collector = visit o.source, collector
|
54
|
+
end
|
55
|
+
|
56
|
+
unless o.wheres.empty?
|
57
|
+
collector << WHERE
|
58
|
+
len = o.wheres.length - 1
|
59
|
+
o.wheres.each_with_index do |x, i|
|
60
|
+
collector = visit(x, collector)
|
61
|
+
collector << AND unless len == i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
unless o.groups.empty?
|
65
|
+
collector << GROUP_BY
|
66
|
+
len = o.groups.length - 1
|
67
|
+
o.groups.each_with_index do |x, i|
|
68
|
+
collector = visit(x, collector)
|
69
|
+
collector << COMMA unless len == i
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if Rails::VERSION::MAJOR < 5
|
74
|
+
collector = maybe_visit o.having, collector
|
75
|
+
else
|
76
|
+
unless o.havings.empty?
|
77
|
+
collector << " HAVING "
|
78
|
+
inject_join o.havings, collector, AND
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
collector
|
83
|
+
end
|
84
|
+
|
85
|
+
def visit_Arel_Nodes_Limit o, collector
|
86
|
+
collector << " ROWS "
|
87
|
+
visit o.expr, collector
|
88
|
+
end
|
89
|
+
|
90
|
+
def visit_Arel_Nodes_Offset o, collector
|
91
|
+
collector << " SKIP "
|
92
|
+
visit o.expr, collector
|
93
|
+
end
|
94
|
+
|
95
|
+
def limit_with_rows o, collector
|
96
|
+
o.offset.expr.value = ActiveModel::Attribute.with_cast_value("OFFSET".freeze,
|
97
|
+
o.offset.expr.value.value + 1,
|
98
|
+
ActiveModel::Type.default_value)
|
99
|
+
offset = o.offset.expr.value
|
100
|
+
o.limit.expr.value = ActiveModel::Attribute.with_cast_value("LIMIT".freeze,
|
101
|
+
(o.limit.expr.value.value) + (offset.value - 1),
|
102
|
+
ActiveModel::Type.default_value)
|
103
|
+
limit = o.limit.expr.value
|
104
|
+
collector << " ROWS "
|
105
|
+
collector.add_bind(offset) {|i| "?"}
|
106
|
+
collector << " TO "
|
107
|
+
collector.add_bind(limit) {|i| "?"}
|
108
|
+
end
|
109
|
+
|
110
|
+
def quote_column_name name
|
111
|
+
return name if Arel::Nodes::SqlLiteral === name
|
112
|
+
@connection.quote_column_name(name)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-rdb-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrey Lobanov (RedSoft)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fb
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.9'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.1'
|
41
|
+
description: ActiveRecord Firebird and RedDatabase Adapter for Rails 5+
|
42
|
+
email: andrey.lobanov@red-soft.ru
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- lib/active_record/connection_adapters/rdb/database_limits.rb
|
49
|
+
- lib/active_record/connection_adapters/rdb/database_statements.rb
|
50
|
+
- lib/active_record/connection_adapters/rdb/quoting.rb
|
51
|
+
- lib/active_record/connection_adapters/rdb/schema_creation.rb
|
52
|
+
- lib/active_record/connection_adapters/rdb/schema_dumper.rb
|
53
|
+
- lib/active_record/connection_adapters/rdb/schema_statements.rb
|
54
|
+
- lib/active_record/connection_adapters/rdb/table_definition.rb
|
55
|
+
- lib/active_record/connection_adapters/rdb_adapter.rb
|
56
|
+
- lib/active_record/connection_adapters/rdb_column.rb
|
57
|
+
- lib/active_record/rdb_base.rb
|
58
|
+
- lib/active_record/tasks/rdb_database_tasks.rb
|
59
|
+
- lib/activerecord-rdb-adapter.rb
|
60
|
+
- lib/arel/visitors/rdb_visitor.rb
|
61
|
+
homepage: http://gitlab.red-soft.biz/andrey.lobanov/activerecord-rdb-adapter
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements:
|
80
|
+
- Firebird library fb
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 2.7.6
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: ActiveRecord Firebird and RedDatabase Adapter
|
86
|
+
test_files: []
|