activerecord-fb-adapter 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []