activerecord-fb-adapter 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/active_record/connection_adapters/fb_adapter.rb +951 -0
- metadata +63 -0
@@ -0,0 +1,951 @@
|
|
1
|
+
# Rails 3-specific database adapter for Firebird (http://firebirdsql.org)
|
2
|
+
# Author: Brent Rowland <rowland@rowlandresearch.com>
|
3
|
+
# Based originally on FireRuby extension by Ken Kunz <kennethkunz@gmail.com>
|
4
|
+
|
5
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
6
|
+
# require 'active_support/core_ext/kernel/requires'
|
7
|
+
require 'base64'
|
8
|
+
|
9
|
+
module Arel
|
10
|
+
module Visitors
|
11
|
+
class FB < Arel::Visitors::ToSql
|
12
|
+
protected
|
13
|
+
|
14
|
+
def visit_Arel_Nodes_SelectStatement(o)
|
15
|
+
select_core = o.cores.map { |x| visit_Arel_Nodes_SelectCore(x) }.join
|
16
|
+
select_core.sub!(/^\s*SELECT/i, "SELECT #{visit(o.offset)}") if o.offset && !o.limit
|
17
|
+
[
|
18
|
+
select_core,
|
19
|
+
("ORDER BY #{o.orders.map { |x| visit(x) }.join(', ')}" unless o.orders.empty?),
|
20
|
+
(limit_offset(o) if o.limit && o.offset),
|
21
|
+
(visit(o.limit) if o.limit && !o.offset),
|
22
|
+
].compact.join ' '
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_Arel_Nodes_UpdateStatement o
|
26
|
+
[
|
27
|
+
"UPDATE #{visit o.relation}",
|
28
|
+
("SET #{o.values.map { |value| visit(value) }.join ', '}" unless o.values.empty?),
|
29
|
+
("WHERE #{o.wheres.map { |x| visit(x) }.join ' AND '}" unless o.wheres.empty?),
|
30
|
+
(visit(o.limit) if o.limit),
|
31
|
+
].compact.join ' '
|
32
|
+
end
|
33
|
+
|
34
|
+
def visit_Arel_Nodes_Limit(o)
|
35
|
+
"ROWS #{visit(o.expr)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def visit_Arel_Nodes_Offset(o)
|
39
|
+
"SKIP #{visit(o.expr)}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def limit_offset(o)
|
44
|
+
"ROWS #{visit(o.offset.expr) + 1} TO #{visit(o.offset.expr) + visit(o.limit.expr)}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Arel::Visitors::VISITORS['fb'] = Arel::Visitors::FB
|
51
|
+
|
52
|
+
module ActiveRecord
|
53
|
+
class << Base
|
54
|
+
def fb_connection(config) # :nodoc:
|
55
|
+
config = config.symbolize_keys.merge(:downcase_names => true)
|
56
|
+
unless config.has_key?(:database)
|
57
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
58
|
+
end
|
59
|
+
config[:database] = File.expand_path(config[:database]) if config[:host] =~ /localhost/i
|
60
|
+
config[:database] = "#{config[:host]}/#{config[:port] || 3050}:#{config[:database]}" if config[:host]
|
61
|
+
require 'fb'
|
62
|
+
db = Fb::Database.new(config)
|
63
|
+
begin
|
64
|
+
connection = db.connect
|
65
|
+
rescue
|
66
|
+
require 'pp'
|
67
|
+
pp config unless config[:create]
|
68
|
+
connection = config[:create] ? db.create.connect : (raise ConnectionNotEstablished, "No Firebird connections established.")
|
69
|
+
end
|
70
|
+
ConnectionAdapters::FbAdapter.new(connection, logger, config)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module ConnectionAdapters # :nodoc:
|
75
|
+
class FbColumn < Column # :nodoc:
|
76
|
+
def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
|
77
|
+
@firebird_type = Fb::SqlType.from_code(type, sub_type || 0)
|
78
|
+
super(name.downcase, nil, @firebird_type, !null_flag)
|
79
|
+
@default = parse_default(default_source) if default_source
|
80
|
+
@limit = (@firebird_type == 'BLOB') ? 10 * 1024 * 1024 : length
|
81
|
+
@domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale
|
82
|
+
end
|
83
|
+
|
84
|
+
def type
|
85
|
+
if @domain =~ /BOOLEAN/
|
86
|
+
:boolean
|
87
|
+
elsif @type == :binary and @sub_type == 1
|
88
|
+
:text
|
89
|
+
else
|
90
|
+
@type
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
|
95
|
+
# This enables Firebird to provide an actual value when context variables are used as column
|
96
|
+
# defaults (such as CURRENT_TIMESTAMP).
|
97
|
+
def default
|
98
|
+
if @default
|
99
|
+
sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
|
100
|
+
connection = ActiveRecord::Base.connection
|
101
|
+
if connection
|
102
|
+
type_cast connection.select_one(sql)['cast']
|
103
|
+
else
|
104
|
+
raise ConnectionNotEstablished, "No Firebird connections established."
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.value_to_boolean(value)
|
110
|
+
%W(#{FbAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def parse_default(default_source)
|
115
|
+
default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i
|
116
|
+
return $1 unless $1.upcase == "NULL"
|
117
|
+
end
|
118
|
+
|
119
|
+
def column_def
|
120
|
+
case @firebird_type
|
121
|
+
when 'CHAR', 'VARCHAR' then "#{@firebird_type}(#{@limit})"
|
122
|
+
when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})"
|
123
|
+
#when 'DOUBLE' then "DOUBLE PRECISION"
|
124
|
+
else @firebird_type
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def simplified_type(field_type)
|
129
|
+
if field_type == 'TIMESTAMP'
|
130
|
+
:datetime
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# The Fb adapter relies on the Fb extension.
|
138
|
+
#
|
139
|
+
# == Usage Notes
|
140
|
+
#
|
141
|
+
# === Sequence (Generator) Names
|
142
|
+
# The Fb adapter supports the same approach adopted for the Oracle
|
143
|
+
# adapter. See ActiveRecord::Base#set_sequence_name for more details.
|
144
|
+
#
|
145
|
+
# Note that in general there is no need to create a <tt>BEFORE INSERT</tt>
|
146
|
+
# trigger corresponding to a Firebird sequence generator when using
|
147
|
+
# ActiveRecord. In other words, you don't have to try to make Firebird
|
148
|
+
# simulate an <tt>AUTO_INCREMENT</tt> or +IDENTITY+ column. When saving a
|
149
|
+
# new record, ActiveRecord pre-fetches the next sequence value for the table
|
150
|
+
# and explicitly includes it in the +INSERT+ statement. (Pre-fetching the
|
151
|
+
# next primary key value is the only reliable method for the Fb
|
152
|
+
# adapter to report back the +id+ after a successful insert.)
|
153
|
+
#
|
154
|
+
# === BOOLEAN Domain
|
155
|
+
# Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily
|
156
|
+
# define a +BOOLEAN+ _domain_ for this purpose, e.g.:
|
157
|
+
#
|
158
|
+
# CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1));
|
159
|
+
#
|
160
|
+
# When the Fb adapter encounters a column that is based on a domain
|
161
|
+
# that includes "BOOLEAN" in the domain name, it will attempt to treat
|
162
|
+
# the column as a +BOOLEAN+.
|
163
|
+
#
|
164
|
+
# By default, the Fb adapter will assume that the BOOLEAN domain is
|
165
|
+
# defined as above. This can be modified if needed. For example, if you
|
166
|
+
# have a legacy schema with the following +BOOLEAN+ domain defined:
|
167
|
+
#
|
168
|
+
# CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F'));
|
169
|
+
#
|
170
|
+
# ...you can add the following line to your <tt>environment.rb</tt> file:
|
171
|
+
#
|
172
|
+
# ActiveRecord::ConnectionAdapters::Fb.boolean_domain = { :true => 'T', :false => 'F' }
|
173
|
+
#
|
174
|
+
# === Column Name Case Semantics
|
175
|
+
# Firebird and ActiveRecord have somewhat conflicting case semantics for
|
176
|
+
# column names.
|
177
|
+
#
|
178
|
+
# [*Firebird*]
|
179
|
+
# The standard practice is to use unquoted column names, which can be
|
180
|
+
# thought of as case-insensitive. (In fact, Firebird converts them to
|
181
|
+
# uppercase.) Quoted column names (not typically used) are case-sensitive.
|
182
|
+
# [*ActiveRecord*]
|
183
|
+
# Attribute accessors corresponding to column names are case-sensitive.
|
184
|
+
# The defaults for primary key and inheritance columns are lowercase, and
|
185
|
+
# in general, people use lowercase attribute names.
|
186
|
+
#
|
187
|
+
# In order to map between the differing semantics in a way that conforms
|
188
|
+
# to common usage for both Firebird and ActiveRecord, uppercase column names
|
189
|
+
# in Firebird are converted to lowercase attribute names in ActiveRecord,
|
190
|
+
# and vice-versa. Mixed-case column names retain their case in both
|
191
|
+
# directions. Lowercase (quoted) Firebird column names are not supported.
|
192
|
+
# This is similar to the solutions adopted by other adapters.
|
193
|
+
#
|
194
|
+
# In general, the best approach is to use unquoted (case-insensitive) column
|
195
|
+
# names in your Firebird DDL (or if you must quote, use uppercase column
|
196
|
+
# names). These will correspond to lowercase attributes in ActiveRecord.
|
197
|
+
#
|
198
|
+
# For example, a Firebird table based on the following DDL:
|
199
|
+
#
|
200
|
+
# CREATE TABLE products (
|
201
|
+
# id BIGINT NOT NULL PRIMARY KEY,
|
202
|
+
# "TYPE" VARCHAR(50),
|
203
|
+
# name VARCHAR(255) );
|
204
|
+
#
|
205
|
+
# ...will correspond to an ActiveRecord model class called +Product+ with
|
206
|
+
# the following attributes: +id+, +type+, +name+.
|
207
|
+
#
|
208
|
+
# ==== Quoting <tt>"TYPE"</tt> and other Firebird reserved words:
|
209
|
+
# In ActiveRecord, the default inheritance column name is +type+. The word
|
210
|
+
# _type_ is a Firebird reserved word, so it must be quoted in any Firebird
|
211
|
+
# SQL statements. Because of the case mapping described above, you should
|
212
|
+
# always reference this column using quoted-uppercase syntax
|
213
|
+
# (<tt>"TYPE"</tt>) within Firebird DDL or other SQL statements (as in the
|
214
|
+
# example above). This holds true for any other Firebird reserved words used
|
215
|
+
# as column names as well.
|
216
|
+
#
|
217
|
+
# === Migrations
|
218
|
+
# The Fb adapter does not currently support Migrations.
|
219
|
+
#
|
220
|
+
# == Connection Options
|
221
|
+
# The following options are supported by the Fb adapter.
|
222
|
+
#
|
223
|
+
# <tt>:database</tt>::
|
224
|
+
# <i>Required option.</i> Specifies one of: (i) a Firebird database alias;
|
225
|
+
# (ii) the full path of a database file; _or_ (iii) a full Firebird
|
226
|
+
# connection string. <i>Do not specify <tt>:host</tt>, <tt>:service</tt>
|
227
|
+
# or <tt>:port</tt> as separate options when using a full connection
|
228
|
+
# string.</i>
|
229
|
+
# <tt>:username</tt>::
|
230
|
+
# Specifies the database user. Defaults to 'sysdba'.
|
231
|
+
# <tt>:password</tt>::
|
232
|
+
# Specifies the database password. Defaults to 'masterkey'.
|
233
|
+
# <tt>:charset</tt>::
|
234
|
+
# Specifies the character set to be used by the connection. Refer to the
|
235
|
+
# Firebird documentation for valid options.
|
236
|
+
class FbAdapter < AbstractAdapter
|
237
|
+
@@boolean_domain = { :true => 1, :false => 0, :name => 'BOOLEAN', :type => 'integer' }
|
238
|
+
cattr_accessor :boolean_domain
|
239
|
+
|
240
|
+
def initialize(connection, logger, connection_params=nil)
|
241
|
+
super(connection, logger)
|
242
|
+
@connection_params = connection_params
|
243
|
+
@visitor = Arel::Visitors::FB.new(self)
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.visitor_for(pool) # :nodoc:
|
247
|
+
Arel::Visitors::FB.new(pool)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns the human-readable name of the adapter. Use mixed case - one
|
251
|
+
# can always use downcase if needed.
|
252
|
+
def adapter_name
|
253
|
+
'Fb'
|
254
|
+
end
|
255
|
+
|
256
|
+
# Does this adapter support migrations? Backend specific, as the
|
257
|
+
# abstract adapter always returns +false+.
|
258
|
+
def supports_migrations?
|
259
|
+
true
|
260
|
+
end
|
261
|
+
|
262
|
+
# Can this adapter determine the primary key for tables not attached
|
263
|
+
# to an Active Record class, such as join tables? Backend specific, as
|
264
|
+
# the abstract adapter always returns +false+.
|
265
|
+
def supports_primary_key?
|
266
|
+
true
|
267
|
+
end
|
268
|
+
|
269
|
+
# Does this adapter support using DISTINCT within COUNT? This is +true+
|
270
|
+
# for all adapters except sqlite.
|
271
|
+
def supports_count_distinct?
|
272
|
+
true
|
273
|
+
end
|
274
|
+
|
275
|
+
# Does this adapter support DDL rollbacks in transactions? That is, would
|
276
|
+
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
|
277
|
+
# SQL Server, and others support this. MySQL and others do not.
|
278
|
+
def supports_ddl_transactions?
|
279
|
+
false
|
280
|
+
end
|
281
|
+
|
282
|
+
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
|
283
|
+
# does not.
|
284
|
+
def supports_savepoints?
|
285
|
+
true
|
286
|
+
end
|
287
|
+
|
288
|
+
# Should primary key values be selected from their corresponding
|
289
|
+
# sequence before the insert statement? If true, next_sequence_value
|
290
|
+
# is called before each insert to set the record's primary key.
|
291
|
+
# This is false for all adapters but Firebird.
|
292
|
+
def prefetch_primary_key?(table_name = nil)
|
293
|
+
true
|
294
|
+
end
|
295
|
+
|
296
|
+
# Does this adapter restrict the number of ids you can use in a list. Oracle has a limit of 1000.
|
297
|
+
def ids_in_list_limit
|
298
|
+
1499
|
299
|
+
end
|
300
|
+
|
301
|
+
# REFERENTIAL INTEGRITY ====================================
|
302
|
+
|
303
|
+
# Override to turn off referential integrity while executing <tt>&block</tt>.
|
304
|
+
# def disable_referential_integrity
|
305
|
+
# yield
|
306
|
+
# end
|
307
|
+
|
308
|
+
# CONNECTION MANAGEMENT ====================================
|
309
|
+
|
310
|
+
# Checks whether the connection to the database is still active. This includes
|
311
|
+
# checking whether the database is actually capable of responding, i.e. whether
|
312
|
+
# the connection isn't stale.
|
313
|
+
def active?
|
314
|
+
return false unless @connection.open?
|
315
|
+
# return true if @connection.transaction_started
|
316
|
+
select("SELECT 1 FROM RDB$DATABASE")
|
317
|
+
true
|
318
|
+
rescue
|
319
|
+
false
|
320
|
+
end
|
321
|
+
|
322
|
+
# Disconnects from the database if already connected, and establishes a
|
323
|
+
# new connection with the database.
|
324
|
+
def reconnect!
|
325
|
+
disconnect!
|
326
|
+
@connection = Fb::Database.connect(@connection_params)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Disconnects from the database if already connected. Otherwise, this
|
330
|
+
# method does nothing.
|
331
|
+
def disconnect!
|
332
|
+
@connection.close rescue nil
|
333
|
+
end
|
334
|
+
|
335
|
+
# Reset the state of this connection, directing the DBMS to clear
|
336
|
+
# transactions and other connection-related server-side state. Usually a
|
337
|
+
# database-dependent operation.
|
338
|
+
#
|
339
|
+
# The default implementation does nothing; the implementation should be
|
340
|
+
# overridden by concrete adapters.
|
341
|
+
def reset!
|
342
|
+
reconnect!
|
343
|
+
end
|
344
|
+
|
345
|
+
# Returns true if its required to reload the connection between requests for development mode.
|
346
|
+
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
|
347
|
+
# def requires_reloading?
|
348
|
+
# false
|
349
|
+
# end
|
350
|
+
|
351
|
+
# Checks whether the connection to the database is still active (i.e. not stale).
|
352
|
+
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
353
|
+
# is no longer active, then this method will reconnect to the database.
|
354
|
+
# def verify!(*ignored)
|
355
|
+
# reconnect! unless active?
|
356
|
+
# end
|
357
|
+
|
358
|
+
# Provides access to the underlying database driver for this adapter. For
|
359
|
+
# example, this method returns a Mysql object in case of MysqlAdapter,
|
360
|
+
# and a PGconn object in case of PostgreSQLAdapter.
|
361
|
+
#
|
362
|
+
# This is useful for when you need to call a proprietary method such as
|
363
|
+
# PostgreSQL's lo_* methods.
|
364
|
+
# def raw_connection
|
365
|
+
# @connection
|
366
|
+
# end
|
367
|
+
|
368
|
+
# def open_transactions
|
369
|
+
# @open_transactions ||= 0
|
370
|
+
# end
|
371
|
+
|
372
|
+
# def increment_open_transactions
|
373
|
+
# @open_transactions ||= 0
|
374
|
+
# @open_transactions += 1
|
375
|
+
# end
|
376
|
+
|
377
|
+
# def decrement_open_transactions
|
378
|
+
# @open_transactions -= 1
|
379
|
+
# end
|
380
|
+
|
381
|
+
# def transaction_joinable=(joinable)
|
382
|
+
# @transaction_joinable = joinable
|
383
|
+
# end
|
384
|
+
|
385
|
+
def create_savepoint
|
386
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
387
|
+
end
|
388
|
+
|
389
|
+
def rollback_to_savepoint
|
390
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
391
|
+
end
|
392
|
+
|
393
|
+
def release_savepoint
|
394
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
395
|
+
end
|
396
|
+
|
397
|
+
# def current_savepoint_name
|
398
|
+
# "active_record_#{open_transactions}"
|
399
|
+
# end
|
400
|
+
|
401
|
+
protected
|
402
|
+
def translate(sql)
|
403
|
+
sql.gsub!(/\bIN\s+\(NULL\)/i, 'IS NULL')
|
404
|
+
sql.sub!(/\bWHERE\s.*$/im) do |m|
|
405
|
+
m.gsub(/\s=\s*NULL\b/i, ' IS NULL')
|
406
|
+
end
|
407
|
+
sql.gsub!(/\sIN\s+\([^\)]*\)/mi) do |m|
|
408
|
+
m.gsub(/\(([^\)]*)\)/m) { |n| n.gsub(/\@(.*?)\@/m) { |n| "'#{quote_string(Base64.decode64(n[1..-1]))}'" } }
|
409
|
+
end
|
410
|
+
args = []
|
411
|
+
sql.gsub!(/\@(.*?)\@/m) { |m| args << Base64.decode64(m[1..-1]); '?' }
|
412
|
+
yield(sql, args) if block_given?
|
413
|
+
end
|
414
|
+
|
415
|
+
def expand(sql, args)
|
416
|
+
([sql] + args) * ', '
|
417
|
+
end
|
418
|
+
|
419
|
+
# def log(sql, args, name, &block)
|
420
|
+
# super(expand(sql, args), name, &block)
|
421
|
+
# end
|
422
|
+
|
423
|
+
def translate_exception(e, message)
|
424
|
+
case e.message
|
425
|
+
when /violation of FOREIGN KEY constraint/
|
426
|
+
InvalidForeignKey.new(message, e)
|
427
|
+
when /violation of PRIMARY or UNIQUE KEY constraint/
|
428
|
+
RecordNotUnique.new(message, e)
|
429
|
+
else
|
430
|
+
super
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
public
|
435
|
+
# from module Quoting
|
436
|
+
def quote(value, column = nil)
|
437
|
+
# records are quoted as their primary key
|
438
|
+
return value.quoted_id if value.respond_to?(:quoted_id)
|
439
|
+
|
440
|
+
case value
|
441
|
+
when String, ActiveSupport::Multibyte::Chars
|
442
|
+
value = value.to_s
|
443
|
+
if column && [:integer, :float].include?(column.type)
|
444
|
+
value = column.type == :integer ? value.to_i : value.to_f
|
445
|
+
value.to_s
|
446
|
+
elsif column && column.type != :binary && value.size < 256
|
447
|
+
"'#{quote_string(value)}'"
|
448
|
+
else
|
449
|
+
"@#{Base64.encode64(value).chop}@"
|
450
|
+
end
|
451
|
+
when NilClass then "NULL"
|
452
|
+
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
|
453
|
+
when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
|
454
|
+
when Float, Fixnum, Bignum then value.to_s
|
455
|
+
# BigDecimals need to be output in a non-normalized form and quoted.
|
456
|
+
when BigDecimal then value.to_s('F')
|
457
|
+
when Symbol then "'#{quote_string(value.to_s)}'"
|
458
|
+
else
|
459
|
+
if value.acts_like?(:date)
|
460
|
+
quote_date(value)
|
461
|
+
elsif value.acts_like?(:time)
|
462
|
+
quote_timestamp(value)
|
463
|
+
else
|
464
|
+
quote_object(value)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
def quote_date(value)
|
470
|
+
"@#{Base64.encode64(value.strftime('%Y-%m-%d')).chop}@"
|
471
|
+
end
|
472
|
+
|
473
|
+
def quote_timestamp(value)
|
474
|
+
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
475
|
+
value = value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
|
476
|
+
"@#{Base64.encode64(value.strftime('%Y-%m-%d %H:%M:%S')).chop}@"
|
477
|
+
end
|
478
|
+
|
479
|
+
def quote_string(string) # :nodoc:
|
480
|
+
string.gsub(/'/, "''")
|
481
|
+
end
|
482
|
+
|
483
|
+
def quote_object(obj)
|
484
|
+
if obj.respond_to?(:to_str)
|
485
|
+
"@#{Base64.encode64(obj.to_str).chop}@"
|
486
|
+
else
|
487
|
+
"@#{Base64.encode64(obj.to_yaml).chop}@"
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
def quote_column_name(column_name) # :nodoc:
|
492
|
+
%Q("#{ar_to_fb_case(column_name.to_s)}")
|
493
|
+
end
|
494
|
+
|
495
|
+
# Quotes the table name. Defaults to column name quoting.
|
496
|
+
# def quote_table_name(table_name)
|
497
|
+
# quote_column_name(table_name)
|
498
|
+
# end
|
499
|
+
|
500
|
+
def quoted_true # :nodoc:
|
501
|
+
quote(boolean_domain[:true])
|
502
|
+
end
|
503
|
+
|
504
|
+
def quoted_false # :nodoc:
|
505
|
+
quote(boolean_domain[:false])
|
506
|
+
end
|
507
|
+
|
508
|
+
private
|
509
|
+
# Maps uppercase Firebird column names to lowercase for ActiveRecord;
|
510
|
+
# mixed-case columns retain their original case.
|
511
|
+
def fb_to_ar_case(column_name)
|
512
|
+
column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase
|
513
|
+
end
|
514
|
+
|
515
|
+
# Maps lowercase ActiveRecord column names to uppercase for Fierbird;
|
516
|
+
# mixed-case columns retain their original case.
|
517
|
+
def ar_to_fb_case(column_name)
|
518
|
+
column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
|
519
|
+
end
|
520
|
+
|
521
|
+
public
|
522
|
+
# from module DatabaseStatements
|
523
|
+
|
524
|
+
# Returns an array of record hashes with the column names as keys and
|
525
|
+
# column values as values.
|
526
|
+
# def select_all(sql, name = nil, format = :hash) # :nodoc:
|
527
|
+
# translate(sql) do |sql, args|
|
528
|
+
# log(sql, args, name) do
|
529
|
+
# @connection.query(format, sql, *args)
|
530
|
+
# end
|
531
|
+
# end
|
532
|
+
# end
|
533
|
+
# Returns an array of record hashes with the column names as keys and
|
534
|
+
# column values as values.
|
535
|
+
def select_all(arel, name = nil, binds = [])
|
536
|
+
select(to_sql(arel, binds), name, binds)
|
537
|
+
end
|
538
|
+
|
539
|
+
# Returns an array of arrays containing the field values.
|
540
|
+
# Order is the same as that returned by +columns+.
|
541
|
+
def select_rows(sql, name = nil)
|
542
|
+
log(sql, name) do
|
543
|
+
@connection.query(:array, sql)
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
# Executes the SQL statement in the context of this connection.
|
548
|
+
# def execute(sql, name = nil, skip_logging = false)
|
549
|
+
# translate(sql) do |sql, args|
|
550
|
+
# if (name == :skip_logging) or skip_logging
|
551
|
+
# @connection.execute(sql, *args)
|
552
|
+
# else
|
553
|
+
# log(sql, args, name) do
|
554
|
+
# @connection.execute(sql, *args)
|
555
|
+
# end
|
556
|
+
# end
|
557
|
+
# end
|
558
|
+
# end
|
559
|
+
|
560
|
+
# Executes +sql+ statement in the context of this connection using
|
561
|
+
# +binds+ as the bind substitutes. +name+ is logged along with
|
562
|
+
# the executed +sql+ statement.
|
563
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
564
|
+
if binds.empty?
|
565
|
+
translate(sql) do |sql, args|
|
566
|
+
log(expand(sql, args), name) do
|
567
|
+
@connection.execute(sql, *args)
|
568
|
+
end
|
569
|
+
end
|
570
|
+
else
|
571
|
+
log(sql, name, binds) do
|
572
|
+
args = binds.map { |col, val| type_cast(val, col) }
|
573
|
+
@connection.execute(sql, *args)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
# Returns the last auto-generated ID from the affected table.
|
579
|
+
# def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
|
580
|
+
# execute(sql, name)
|
581
|
+
# id_value
|
582
|
+
# end
|
583
|
+
|
584
|
+
# Executes the update statement and returns the number of rows affected.
|
585
|
+
# alias_method :update, :execute
|
586
|
+
# def update(sql, name = nil)
|
587
|
+
# update_sql(sql, name)
|
588
|
+
# end
|
589
|
+
|
590
|
+
# Executes the delete statement and returns the number of rows affected.
|
591
|
+
# alias_method :delete, :execute
|
592
|
+
# def delete(sql, name = nil)
|
593
|
+
# delete_sql(sql, name)
|
594
|
+
# end
|
595
|
+
|
596
|
+
# Checks whether there is currently no transaction active. This is done
|
597
|
+
# by querying the database driver, and does not use the transaction
|
598
|
+
# house-keeping information recorded by #increment_open_transactions and
|
599
|
+
# friends.
|
600
|
+
#
|
601
|
+
# Returns true if there is no transaction active, false if there is a
|
602
|
+
# transaction active, and nil if this information is unknown.
|
603
|
+
def outside_transaction?
|
604
|
+
!@connection.transaction_started
|
605
|
+
end
|
606
|
+
|
607
|
+
# Begins the transaction (and turns off auto-committing).
|
608
|
+
def begin_db_transaction
|
609
|
+
@transaction = @connection.transaction('READ COMMITTED')
|
610
|
+
end
|
611
|
+
|
612
|
+
# Commits the transaction (and turns on auto-committing).
|
613
|
+
def commit_db_transaction
|
614
|
+
@transaction = @connection.commit
|
615
|
+
end
|
616
|
+
|
617
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
618
|
+
# done if the transaction block raises an exception or returns false.
|
619
|
+
def rollback_db_transaction
|
620
|
+
@transaction = @connection.rollback
|
621
|
+
end
|
622
|
+
|
623
|
+
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
|
624
|
+
# fragment that has the same semantics as LIMIT and OFFSET.
|
625
|
+
#
|
626
|
+
# +options+ must be a Hash which contains a +:limit+ option
|
627
|
+
# and an +:offset+ option.
|
628
|
+
#
|
629
|
+
# This method *modifies* the +sql+ parameter.
|
630
|
+
#
|
631
|
+
# ===== Examples
|
632
|
+
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
|
633
|
+
# generates
|
634
|
+
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
|
635
|
+
def add_limit_offset!(sql, options) # :nodoc:
|
636
|
+
if limit = options[:limit]
|
637
|
+
if offset = options[:offset]
|
638
|
+
sql << " ROWS #{offset.to_i + 1} TO #{offset.to_i + limit.to_i}"
|
639
|
+
else
|
640
|
+
sql << " ROWS #{limit.to_i}"
|
641
|
+
end
|
642
|
+
end
|
643
|
+
sql
|
644
|
+
end
|
645
|
+
|
646
|
+
def default_sequence_name(table_name, column=nil)
|
647
|
+
"#{table_name}_seq"
|
648
|
+
end
|
649
|
+
|
650
|
+
# Set the sequence to the max value of the table's column.
|
651
|
+
def reset_sequence!(table, column, sequence = nil)
|
652
|
+
max_id = select_value("select max(#{column}) from #{table}")
|
653
|
+
execute("alter sequence #{default_sequence_name(table, column)} restart with #{max_id}")
|
654
|
+
end
|
655
|
+
|
656
|
+
def next_sequence_value(sequence_name)
|
657
|
+
select_one("SELECT GEN_ID(#{sequence_name}, 1) FROM RDB$DATABASE").values.first
|
658
|
+
end
|
659
|
+
|
660
|
+
# Inserts the given fixture into the table. Overridden in adapters that require
|
661
|
+
# something beyond a simple insert (eg. Oracle).
|
662
|
+
# def insert_fixture(fixture, table_name)
|
663
|
+
# execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
|
664
|
+
# end
|
665
|
+
|
666
|
+
# def empty_insert_statement_value
|
667
|
+
# "VALUES(DEFAULT)"
|
668
|
+
# end
|
669
|
+
|
670
|
+
# def case_sensitive_equality_operator
|
671
|
+
# "="
|
672
|
+
# end
|
673
|
+
|
674
|
+
protected
|
675
|
+
# Returns an array of record hashes with the column names as keys and
|
676
|
+
# column values as values.
|
677
|
+
def select(sql, name = nil, binds = [])
|
678
|
+
if binds.empty?
|
679
|
+
translate(sql) do |sql, args|
|
680
|
+
log(expand(sql, args), name) do
|
681
|
+
@connection.query(:hash, sql, *args)
|
682
|
+
end
|
683
|
+
end
|
684
|
+
else
|
685
|
+
log(sql, name, binds) do
|
686
|
+
args = binds.map { |col, val| type_cast(val, col) }
|
687
|
+
@connection.query(:hash, sql, *args)
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
public
|
693
|
+
# from module SchemaStatements
|
694
|
+
|
695
|
+
# Returns a Hash of mappings from the abstract data types to the native
|
696
|
+
# database types. See TableDefinition#column for details on the recognized
|
697
|
+
# abstract data types.
|
698
|
+
def native_database_types
|
699
|
+
{
|
700
|
+
:primary_key => "integer not null primary key",
|
701
|
+
:string => { :name => "varchar", :limit => 255 },
|
702
|
+
:text => { :name => "blob sub_type text" },
|
703
|
+
:integer => { :name => "integer" },
|
704
|
+
:float => { :name => "float" },
|
705
|
+
:decimal => { :name => "decimal" },
|
706
|
+
:datetime => { :name => "timestamp" },
|
707
|
+
:timestamp => { :name => "timestamp" },
|
708
|
+
:time => { :name => "time" },
|
709
|
+
:date => { :name => "date" },
|
710
|
+
:binary => { :name => "blob" },
|
711
|
+
:boolean => { :name => boolean_domain[:name] }
|
712
|
+
}
|
713
|
+
end
|
714
|
+
|
715
|
+
# Truncates a table alias according to the limits of the current adapter.
|
716
|
+
# def table_alias_for(table_name)
|
717
|
+
# table_name[0..table_alias_length-1].gsub(/\./, '_')
|
718
|
+
# end
|
719
|
+
|
720
|
+
# def tables(name = nil) end
|
721
|
+
def tables(name = nil)
|
722
|
+
@connection.table_names
|
723
|
+
end
|
724
|
+
|
725
|
+
# Returns an array of indexes for the given table.
|
726
|
+
def indexes(table_name, name = nil)
|
727
|
+
result = @connection.indexes.values.select {|ix| ix.table_name == table_name && ix.index_name !~ /^rdb\$/ }
|
728
|
+
indexes = result.map {|ix| IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns) }
|
729
|
+
indexes
|
730
|
+
end
|
731
|
+
|
732
|
+
def primary_key(table_name) #:nodoc:
|
733
|
+
sql = <<-END_SQL
|
734
|
+
SELECT s.rdb$field_name
|
735
|
+
FROM rdb$indices i
|
736
|
+
JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
|
737
|
+
LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
|
738
|
+
WHERE i.rdb$relation_name = '#{ar_to_fb_case(table_name)}' and c.rdb$constraint_type = 'PRIMARY KEY';
|
739
|
+
END_SQL
|
740
|
+
row = select_one(sql)
|
741
|
+
row && fb_to_ar_case(row.values.first.rstrip)
|
742
|
+
end
|
743
|
+
|
744
|
+
# Returns an array of Column objects for the table specified by +table_name+.
|
745
|
+
# See the concrete implementation for details on the expected parameter values.
|
746
|
+
def columns(table_name, name = nil)
|
747
|
+
sql = <<-END_SQL
|
748
|
+
SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
|
749
|
+
f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
|
750
|
+
COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
|
751
|
+
COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
|
752
|
+
FROM rdb$relation_fields r
|
753
|
+
JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
|
754
|
+
WHERE r.rdb$relation_name = '#{ar_to_fb_case(table_name)}'
|
755
|
+
ORDER BY r.rdb$field_position
|
756
|
+
END_SQL
|
757
|
+
select_rows(sql, name).collect do |field|
|
758
|
+
field_values = field.collect do |value|
|
759
|
+
case value
|
760
|
+
when String then value.rstrip
|
761
|
+
else value
|
762
|
+
end
|
763
|
+
end
|
764
|
+
FbColumn.new(*field_values)
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
def create_table(name, options = {}) # :nodoc:
|
769
|
+
begin
|
770
|
+
super
|
771
|
+
rescue
|
772
|
+
raise unless non_existent_domain_error?
|
773
|
+
create_boolean_domain
|
774
|
+
super
|
775
|
+
end
|
776
|
+
unless options[:id] == false or options[:sequence] == false
|
777
|
+
sequence_name = options[:sequence] || default_sequence_name(name)
|
778
|
+
create_sequence(sequence_name)
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
def drop_table(name, options = {}) # :nodoc:
|
783
|
+
super(name)
|
784
|
+
unless options[:sequence] == false
|
785
|
+
sequence_name = options[:sequence] || default_sequence_name(name)
|
786
|
+
drop_sequence(sequence_name) if sequence_exists?(sequence_name)
|
787
|
+
end
|
788
|
+
end
|
789
|
+
|
790
|
+
# Adds a new column to the named table.
|
791
|
+
# See TableDefinition#column for details of the options you can use.
|
792
|
+
def add_column(table_name, column_name, type, options = {})
|
793
|
+
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])}"
|
794
|
+
add_column_options!(add_column_sql, options)
|
795
|
+
execute(add_column_sql)
|
796
|
+
end
|
797
|
+
|
798
|
+
# Changes the column's definition according to the new options.
|
799
|
+
# See TableDefinition#column for details of the options you can use.
|
800
|
+
# ===== Examples
|
801
|
+
# change_column(:suppliers, :name, :string, :limit => 80)
|
802
|
+
# change_column(:accounts, :description, :text)
|
803
|
+
def change_column(table_name, column_name, type, options = {})
|
804
|
+
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])}"
|
805
|
+
sql = add_column_options(sql, options)
|
806
|
+
execute(sql)
|
807
|
+
end
|
808
|
+
|
809
|
+
# Sets a new default value for a column. If you want to set the default
|
810
|
+
# value to +NULL+, you are out of luck. You need to
|
811
|
+
# DatabaseStatements#execute the appropriate SQL statement yourself.
|
812
|
+
# ===== Examples
|
813
|
+
# change_column_default(:suppliers, :qualification, 'new')
|
814
|
+
# change_column_default(:accounts, :authorized, 1)
|
815
|
+
def change_column_default(table_name, column_name, default)
|
816
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}")
|
817
|
+
end
|
818
|
+
|
819
|
+
# Renames a column.
|
820
|
+
# ===== Example
|
821
|
+
# rename_column(:suppliers, :description, :name)
|
822
|
+
def rename_column(table_name, column_name, new_column_name)
|
823
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
824
|
+
end
|
825
|
+
|
826
|
+
def remove_index!(table_name, index_name) #:nodoc:
|
827
|
+
execute("DROP INDEX #{quote_column_name(index_name)}")
|
828
|
+
end
|
829
|
+
|
830
|
+
def index_name(table_name, options) #:nodoc:
|
831
|
+
if Hash === options # legacy support
|
832
|
+
if options[:column]
|
833
|
+
"#{table_name}_#{Array.wrap(options[:column]) * '_'}"
|
834
|
+
elsif options[:name]
|
835
|
+
options[:name]
|
836
|
+
else
|
837
|
+
raise ArgumentError, "You must specify the index name"
|
838
|
+
end
|
839
|
+
else
|
840
|
+
index_name(table_name, :column => options)
|
841
|
+
end
|
842
|
+
end
|
843
|
+
|
844
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
845
|
+
case type
|
846
|
+
when :integer then integer_to_sql(limit)
|
847
|
+
when :float then float_to_sql(limit)
|
848
|
+
else super
|
849
|
+
end
|
850
|
+
end
|
851
|
+
|
852
|
+
private
|
853
|
+
# Map logical Rails types to Firebird-specific data types.
|
854
|
+
def integer_to_sql(limit)
|
855
|
+
return 'integer' if limit.nil?
|
856
|
+
case limit
|
857
|
+
when 1..2 then 'smallint'
|
858
|
+
when 3..4 then 'integer'
|
859
|
+
when 5..8 then 'bigint'
|
860
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a NUMERIC with PRECISION 0 instead.")
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
def float_to_sql(limit)
|
865
|
+
if limit <= 4
|
866
|
+
'float'
|
867
|
+
else
|
868
|
+
'double precision'
|
869
|
+
end
|
870
|
+
end
|
871
|
+
|
872
|
+
def non_existent_domain_error?
|
873
|
+
$!.message =~ /Specified domain or source column \w+ does not exist/
|
874
|
+
end
|
875
|
+
|
876
|
+
def create_boolean_domain
|
877
|
+
sql = <<-end_sql
|
878
|
+
CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
|
879
|
+
CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
|
880
|
+
end_sql
|
881
|
+
execute(sql)
|
882
|
+
end
|
883
|
+
|
884
|
+
def create_sequence(sequence_name)
|
885
|
+
execute("CREATE SEQUENCE #{sequence_name}")
|
886
|
+
end
|
887
|
+
|
888
|
+
def drop_sequence(sequence_name)
|
889
|
+
execute("DROP SEQUENCE #{sequence_name}")
|
890
|
+
end
|
891
|
+
|
892
|
+
def sequence_exists?(sequence_name)
|
893
|
+
@connection.generator_names.include?(sequence_name)
|
894
|
+
end
|
895
|
+
|
896
|
+
public
|
897
|
+
# from module DatabaseLimits
|
898
|
+
|
899
|
+
# the maximum length of a table alias
|
900
|
+
def table_alias_length
|
901
|
+
31
|
902
|
+
end
|
903
|
+
|
904
|
+
# the maximum length of a column name
|
905
|
+
def column_name_length
|
906
|
+
31
|
907
|
+
end
|
908
|
+
|
909
|
+
# the maximum length of a table name
|
910
|
+
def table_name_length
|
911
|
+
31
|
912
|
+
end
|
913
|
+
|
914
|
+
# the maximum length of an index name
|
915
|
+
def index_name_length
|
916
|
+
31
|
917
|
+
end
|
918
|
+
|
919
|
+
# the maximum number of columns per table
|
920
|
+
# def columns_per_table
|
921
|
+
# 1024
|
922
|
+
# end
|
923
|
+
|
924
|
+
# the maximum number of indexes per table
|
925
|
+
def indexes_per_table
|
926
|
+
65_535
|
927
|
+
end
|
928
|
+
|
929
|
+
# the maximum number of columns in a multicolumn index
|
930
|
+
# def columns_per_multicolumn_index
|
931
|
+
# 16
|
932
|
+
# end
|
933
|
+
|
934
|
+
# the maximum number of elements in an IN (x,y,z) clause
|
935
|
+
def in_clause_length
|
936
|
+
1499
|
937
|
+
end
|
938
|
+
|
939
|
+
# the maximum length of an SQL query
|
940
|
+
def sql_query_length
|
941
|
+
32767
|
942
|
+
end
|
943
|
+
|
944
|
+
# maximum number of joins in a single query
|
945
|
+
# def joins_per_query
|
946
|
+
# 256
|
947
|
+
# end
|
948
|
+
|
949
|
+
end
|
950
|
+
end
|
951
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-fb-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brent Rowland
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: fb
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.7.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.7.0
|
30
|
+
description:
|
31
|
+
email: rowland@rowlandresearch.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/active_record/connection_adapters/fb_adapter.rb
|
37
|
+
homepage: http://github.com/rowland/activerecord-fb-adapter
|
38
|
+
licenses: []
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements:
|
56
|
+
- Firebird library fb
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.8.23
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: ActiveRecord Firebird Adapter for Rails 3. Unlike fb_adapter for Rails 1.x
|
62
|
+
and 2.x, this version attempts to support migrations.
|
63
|
+
test_files: []
|