activerecord-fb-adapter 0.7.0

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.
@@ -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: []