rubyfb 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/CHANGELOG +6 -0
  2. data/LICENSE +411 -0
  3. data/Manifest +73 -0
  4. data/README +460 -0
  5. data/Rakefile +20 -0
  6. data/examples/example01.rb +65 -0
  7. data/ext/AddUser.c +464 -0
  8. data/ext/AddUser.h +37 -0
  9. data/ext/Backup.c +783 -0
  10. data/ext/Backup.h +37 -0
  11. data/ext/Blob.c +421 -0
  12. data/ext/Blob.h +65 -0
  13. data/ext/Common.c +54 -0
  14. data/ext/Common.h +37 -0
  15. data/ext/Connection.c +863 -0
  16. data/ext/Connection.h +50 -0
  17. data/ext/DataArea.c +274 -0
  18. data/ext/DataArea.h +38 -0
  19. data/ext/Database.c +449 -0
  20. data/ext/Database.h +48 -0
  21. data/ext/FireRuby.c +240 -0
  22. data/ext/FireRuby.h +50 -0
  23. data/ext/FireRubyException.c +268 -0
  24. data/ext/FireRubyException.h +51 -0
  25. data/ext/Generator.c +689 -0
  26. data/ext/Generator.h +53 -0
  27. data/ext/RemoveUser.c +212 -0
  28. data/ext/RemoveUser.h +37 -0
  29. data/ext/Restore.c +855 -0
  30. data/ext/Restore.h +37 -0
  31. data/ext/ResultSet.c +809 -0
  32. data/ext/ResultSet.h +60 -0
  33. data/ext/Row.c +965 -0
  34. data/ext/Row.h +55 -0
  35. data/ext/ServiceManager.c +316 -0
  36. data/ext/ServiceManager.h +48 -0
  37. data/ext/Services.c +124 -0
  38. data/ext/Services.h +42 -0
  39. data/ext/Statement.c +785 -0
  40. data/ext/Statement.h +62 -0
  41. data/ext/Transaction.c +684 -0
  42. data/ext/Transaction.h +50 -0
  43. data/ext/TypeMap.c +1182 -0
  44. data/ext/TypeMap.h +51 -0
  45. data/ext/extconf.rb +28 -0
  46. data/ext/mkmf.bat +1 -0
  47. data/lib/SQLType.rb +224 -0
  48. data/lib/active_record/connection_adapters/rubyfb_adapter.rb +805 -0
  49. data/lib/mkdoc +1 -0
  50. data/lib/rubyfb.rb +2 -0
  51. data/lib/rubyfb_lib.so +0 -0
  52. data/lib/src.rb +1800 -0
  53. data/rubyfb.gemspec +31 -0
  54. data/test/AddRemoveUserTest.rb +56 -0
  55. data/test/BackupRestoreTest.rb +99 -0
  56. data/test/BlobTest.rb +57 -0
  57. data/test/CharacterSetTest.rb +63 -0
  58. data/test/ConnectionTest.rb +111 -0
  59. data/test/DDLTest.rb +54 -0
  60. data/test/DatabaseTest.rb +83 -0
  61. data/test/GeneratorTest.rb +50 -0
  62. data/test/KeyTest.rb +140 -0
  63. data/test/ResultSetTest.rb +162 -0
  64. data/test/RoleTest.rb +73 -0
  65. data/test/RowCountTest.rb +65 -0
  66. data/test/RowTest.rb +203 -0
  67. data/test/SQLTest.rb +182 -0
  68. data/test/SQLTypeTest.rb +101 -0
  69. data/test/ServiceManagerTest.rb +29 -0
  70. data/test/StatementTest.rb +135 -0
  71. data/test/TestSetup.rb +11 -0
  72. data/test/TransactionTest.rb +112 -0
  73. data/test/TypeTest.rb +92 -0
  74. data/test/UnitTest.rb +65 -0
  75. metadata +149 -0
@@ -0,0 +1,805 @@
1
+ # Author: Ken Kunz <kennethkunz@gmail.com>
2
+ require 'active_record/connection_adapters/abstract_adapter'
3
+
4
+ module FireRuby # :nodoc: all
5
+ NON_EXISTENT_DOMAIN_ERROR = "335544569"
6
+ class Database
7
+ def self.db_string_for(config)
8
+ unless config.has_key?(:database)
9
+ raise ArgumentError, "No database specified. Missing argument: database."
10
+ end
11
+ host_string = config.values_at(:host, :service, :port).compact.first(2).join("/") if config[:host]
12
+ [host_string, config[:database]].join(":")
13
+ end
14
+
15
+ def self.new_from_config(config)
16
+ db = new db_string_for(config)
17
+ db.character_set = config[:charset]
18
+ return db
19
+ end
20
+ end
21
+ end
22
+
23
+ module ActiveRecord
24
+ class Base
25
+ def self.rubyfb_connection(config) # :nodoc:
26
+ require_library_or_gem 'rubyfb'
27
+ config.symbolize_keys!
28
+ db = FireRuby::Database.new_from_config(config)
29
+ connection_params = config.values_at(:username, :password)
30
+ connection = db.connect(*connection_params)
31
+ ConnectionAdapters::RubyfbAdapter.new(connection, logger, connection_params)
32
+ end
33
+
34
+ after_save :write_blobs
35
+ def write_blobs #:nodoc:
36
+ if connection.is_a?(ConnectionAdapters::RubyfbAdapter)
37
+ connection.write_blobs(self.class.table_name, self.class, attributes)
38
+ end
39
+ end
40
+
41
+ private :write_blobs
42
+ end
43
+
44
+ module ConnectionAdapters
45
+ class FirebirdColumn < Column # :nodoc:
46
+ VARCHAR_MAX_LENGTH = 32_765
47
+
48
+ def initialize(connection, name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
49
+ @firebird_type = FireRuby::SQLType.to_base_type(type, sub_type).to_s
50
+
51
+ super(name.downcase, nil, @firebird_type, !null_flag)
52
+
53
+ @limit = decide_limit(length)
54
+ @domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale.abs
55
+ @type = simplified_type(@firebird_type)
56
+ @default = parse_default(default_source) if default_source
57
+ @default = type_cast(decide_default(connection)) if @default
58
+ end
59
+
60
+ def self.value_to_boolean(value)
61
+ %W(#{RubyfbAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
62
+ end
63
+
64
+ private
65
+ def parse_default(default_source)
66
+ default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i
67
+ return $1 unless $1.upcase == "NULL"
68
+ end
69
+
70
+ def decide_default(connection)
71
+ if @default =~ /^'?(\d*\.?\d+)'?$/ or
72
+ @default =~ /^'(.*)'$/ && [:text, :string, :binary, :boolean].include?(type)
73
+ $1
74
+ else
75
+ firebird_cast_default(connection)
76
+ end
77
+ end
78
+
79
+ # Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
80
+ # This enables Firebird to provide an actual value when context variables are used as column
81
+ # defaults (such as CURRENT_TIMESTAMP).
82
+ def firebird_cast_default(connection)
83
+ sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
84
+ connection.select_rows(sql).first[0]
85
+ end
86
+
87
+ def decide_limit(length)
88
+ if text? or number?
89
+ length
90
+ end
91
+ end
92
+
93
+ def column_def
94
+ case @firebird_type
95
+ when 'BLOB' then "VARCHAR(#{VARCHAR_MAX_LENGTH})"
96
+ when 'CHAR', 'VARCHAR' then "#{@firebird_type}(#{@limit})"
97
+ when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})"
98
+ when 'DOUBLE' then "DOUBLE PRECISION"
99
+ else @firebird_type
100
+ end
101
+ end
102
+
103
+ def simplified_type(field_type)
104
+ case field_type
105
+ when /timestamp/i
106
+ :datetime
107
+ when /decimal|numeric|number/i
108
+ @scale == 0 ? :integer : :decimal
109
+ when /blob/i
110
+ @subtype == 1 ? :text : :binary
111
+ else
112
+ if @domain =~ /boolean/i
113
+ :boolean
114
+ else
115
+ super
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ # The Firebird adapter relies on the FireRuby[http://rubyforge.org/projects/fireruby/]
122
+ # extension, version 0.4.0 or later (available as a gem or from
123
+ # RubyForge[http://rubyforge.org/projects/fireruby/]). FireRuby works with
124
+ # Firebird 1.5.x on Linux, OS X and Win32 platforms.
125
+ #
126
+ # == Usage Notes
127
+ #
128
+ # === Sequence (Generator) Names
129
+ # The Firebird adapter supports the same approach adopted for the Oracle
130
+ # adapter. See ActiveRecord::Base#set_sequence_name for more details.
131
+ #
132
+ # Note that in general there is no need to create a <tt>BEFORE INSERT</tt>
133
+ # trigger corresponding to a Firebird sequence generator when using
134
+ # ActiveRecord. In other words, you don't have to try to make Firebird
135
+ # simulate an <tt>AUTO_INCREMENT</tt> or +IDENTITY+ column. When saving a
136
+ # new record, ActiveRecord pre-fetches the next sequence value for the table
137
+ # and explicitly includes it in the +INSERT+ statement. (Pre-fetching the
138
+ # next primary key value is the only reliable method for the Firebird
139
+ # adapter to report back the +id+ after a successful insert.)
140
+ #
141
+ # === BOOLEAN Domain
142
+ # Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily
143
+ # define a +BOOLEAN+ _domain_ for this purpose, e.g.:
144
+ #
145
+ # CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1) OR VALUE IS NULL);
146
+ #
147
+ # When the Firebird adapter encounters a column that is based on a domain
148
+ # that includes "BOOLEAN" in the domain name, it will attempt to treat
149
+ # the column as a +BOOLEAN+.
150
+ #
151
+ # By default, the Firebird adapter will assume that the BOOLEAN domain is
152
+ # defined as above. This can be modified if needed. For example, if you
153
+ # have a legacy schema with the following +BOOLEAN+ domain defined:
154
+ #
155
+ # CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F'));
156
+ #
157
+ # ...you can add the following line to your <tt>environment.rb</tt> file:
158
+ #
159
+ # ActiveRecord::ConnectionAdapters::RubyfbAdapter.boolean_domain = { :true => 'T', :false => 'F' }
160
+ #
161
+ # === BLOB Elements
162
+ # The Firebird adapter currently provides only limited support for +BLOB+
163
+ # columns. You cannot currently retrieve a +BLOB+ as an IO stream.
164
+ # When selecting a +BLOB+, the entire element is converted into a String.
165
+ # +BLOB+ handling is supported by writing an empty +BLOB+ to the database on
166
+ # insert/update and then executing a second query to save the +BLOB+.
167
+ #
168
+ # === Column Name Case Semantics
169
+ # Firebird and ActiveRecord have somewhat conflicting case semantics for
170
+ # column names.
171
+ #
172
+ # [*Firebird*]
173
+ # The standard practice is to use unquoted column names, which can be
174
+ # thought of as case-insensitive. (In fact, Firebird converts them to
175
+ # uppercase.) Quoted column names (not typically used) are case-sensitive.
176
+ # [*ActiveRecord*]
177
+ # Attribute accessors corresponding to column names are case-sensitive.
178
+ # The defaults for primary key and inheritance columns are lowercase, and
179
+ # in general, people use lowercase attribute names.
180
+ #
181
+ # In order to map between the differing semantics in a way that conforms
182
+ # to common usage for both Firebird and ActiveRecord, uppercase column names
183
+ # in Firebird are converted to lowercase attribute names in ActiveRecord,
184
+ # and vice-versa. Mixed-case column names retain their case in both
185
+ # directions. Lowercase (quoted) Firebird column names are not supported.
186
+ # This is similar to the solutions adopted by other adapters.
187
+ #
188
+ # In general, the best approach is to use unqouted (case-insensitive) column
189
+ # names in your Firebird DDL (or if you must quote, use uppercase column
190
+ # names). These will correspond to lowercase attributes in ActiveRecord.
191
+ #
192
+ # For example, a Firebird table based on the following DDL:
193
+ #
194
+ # CREATE TABLE products (
195
+ # id BIGINT NOT NULL PRIMARY KEY,
196
+ # "TYPE" VARCHAR(50),
197
+ # name VARCHAR(255) );
198
+ #
199
+ # ...will correspond to an ActiveRecord model class called +Product+ with
200
+ # the following attributes: +id+, +type+, +name+.
201
+ #
202
+ # ==== Quoting <tt>"TYPE"</tt> and other Firebird reserved words:
203
+ # In ActiveRecord, the default inheritance column name is +type+. The word
204
+ # _type_ is a Firebird reserved word, so it must be quoted in any Firebird
205
+ # SQL statements. Because of the case mapping described above, you should
206
+ # always reference this column using quoted-uppercase syntax
207
+ # (<tt>"TYPE"</tt>) within Firebird DDL or other SQL statements (as in the
208
+ # example above). This holds true for any other Firebird reserved words used
209
+ # as column names as well.
210
+ #
211
+ # === Migrations
212
+ # The Firebird Adapter now supports Migrations.
213
+ #
214
+ # ==== Create/Drop Table and Sequence Generators
215
+ # Creating or dropping a table will automatically create/drop a
216
+ # correpsonding sequence generator, using the default naming convension.
217
+ # You can specify a different name using the <tt>:sequence</tt> option; no
218
+ # generator is created if <tt>:sequence</tt> is set to +false+.
219
+ #
220
+ # ==== Rename Table
221
+ # The Firebird #rename_table Migration should be used with caution.
222
+ # Firebird 1.5 lacks built-in support for this feature, so it is
223
+ # implemented by making a copy of the original table (including column
224
+ # definitions, indexes and data records), and then dropping the original
225
+ # table. Constraints and Triggers are _not_ properly copied, so avoid
226
+ # this method if your original table includes constraints (other than
227
+ # the primary key) or triggers. (Consider manually copying your table
228
+ # or using a view instead.)
229
+ #
230
+ # == Connection Options
231
+ # The following options are supported by the Firebird adapter. None of the
232
+ # options have default values.
233
+ #
234
+ # <tt>:database</tt>::
235
+ # <i>Required option.</i> Specifies one of: (i) a Firebird database alias;
236
+ # (ii) the full path of a database file; _or_ (iii) a full Firebird
237
+ # connection string. <i>Do not specify <tt>:host</tt>, <tt>:service</tt>
238
+ # or <tt>:port</tt> as separate options when using a full connection
239
+ # string.</i>
240
+ # <tt>:host</tt>::
241
+ # Set to <tt>"remote.host.name"</tt> for remote database connections.
242
+ # May be omitted for local connections if a full database path is
243
+ # specified for <tt>:database</tt>. Some platforms require a value of
244
+ # <tt>"localhost"</tt> for local connections when using a Firebird
245
+ # database _alias_.
246
+ # <tt>:service</tt>::
247
+ # Specifies a service name for the connection. Only used if <tt>:host</tt>
248
+ # is provided. Required when connecting to a non-standard service.
249
+ # <tt>:port</tt>::
250
+ # Specifies the connection port. Only used if <tt>:host</tt> is provided
251
+ # and <tt>:service</tt> is not. Required when connecting to a non-standard
252
+ # port and <tt>:service</tt> is not defined.
253
+ # <tt>:username</tt>::
254
+ # Specifies the database user. May be omitted or set to +nil+ (together
255
+ # with <tt>:password</tt>) to use the underlying operating system user
256
+ # credentials on supported platforms.
257
+ # <tt>:password</tt>::
258
+ # Specifies the database password. Must be provided if <tt>:username</tt>
259
+ # is explicitly specified; should be omitted if OS user credentials are
260
+ # are being used.
261
+ # <tt>:charset</tt>::
262
+ # Specifies the character set to be used by the connection. Refer to
263
+ # Firebird documentation for valid options.
264
+ class RubyfbAdapter < AbstractAdapter
265
+ TEMP_COLUMN_NAME = 'AR$TEMP_COLUMN'
266
+
267
+ @@boolean_domain = { :name => "d_boolean", :type => "smallint", :true => 1, :false => 0 }
268
+ cattr_accessor :boolean_domain
269
+
270
+ def initialize(connection, logger, connection_params = nil)
271
+ super(connection, logger)
272
+ @connection_params = connection_params
273
+ end
274
+
275
+ def adapter_name # :nodoc:
276
+ 'Rubyfb'
277
+ end
278
+
279
+ def supports_migrations? # :nodoc:
280
+ true
281
+ end
282
+
283
+ def native_database_types # :nodoc:
284
+ {
285
+ :primary_key => "BIGINT NOT NULL PRIMARY KEY",
286
+ :string => { :name => "varchar", :limit => 255 },
287
+ :text => { :name => "blob sub_type text" },
288
+ :integer => { :name => "bigint" },
289
+ :decimal => { :name => "decimal" },
290
+ :numeric => { :name => "numeric" },
291
+ :float => { :name => "float" },
292
+ :datetime => { :name => "timestamp" },
293
+ :timestamp => { :name => "timestamp" },
294
+ :time => { :name => "time" },
295
+ :date => { :name => "date" },
296
+ :binary => { :name => "blob sub_type 0" },
297
+ :boolean => boolean_domain
298
+ }
299
+ end
300
+
301
+ # Returns true for Firebird adapter (since Firebird requires primary key
302
+ # values to be pre-fetched before insert). See also #next_sequence_value.
303
+ def prefetch_primary_key?(table_name = nil)
304
+ true
305
+ end
306
+
307
+ def default_sequence_name(table_name, primary_key = nil) # :nodoc:
308
+ "#{table_name}_seq"
309
+ end
310
+
311
+
312
+ # QUOTING ==================================================
313
+
314
+ # We use quoting in order to implement BLOB handling. In order to
315
+ # do this we quote a BLOB to an empty string which will force Firebird
316
+ # to create an empty BLOB in the db for us. Quoting is used in some
317
+ # other places besides insert/update like for column defaults. That is
318
+ # why we are checking caller to see where we're coming from. This isn't
319
+ # perfect but It works.
320
+ def quote(value, column = nil) # :nodoc:
321
+ if [Time, DateTime].include?(value.class)
322
+ "CAST('#{value.strftime("%Y-%m-%d %H:%M:%S")}' AS TIMESTAMP)"
323
+ elsif value && column && [:text, :binary].include?(column.type) && caller.to_s !~ /add_column_options!/i
324
+ "''"
325
+ else
326
+ super
327
+ end
328
+ end
329
+
330
+ def quote_string(string) # :nodoc:
331
+ string.gsub(/'/, "''")
332
+ end
333
+
334
+ def quote_column_name(column_name) # :nodoc:
335
+ %Q("#{ar_to_fb_case(column_name.to_s)}")
336
+ end
337
+
338
+ def quoted_true # :nodoc:
339
+ quote(boolean_domain[:true])
340
+ end
341
+
342
+ def quoted_false # :nodoc:
343
+ quote(boolean_domain[:false])
344
+ end
345
+
346
+
347
+ # CONNECTION MANAGEMENT ====================================
348
+
349
+ def active? # :nodoc:
350
+ return false if @connection.closed?
351
+ begin
352
+ execute('select first 1 cast(1 as smallint) from rdb$database')
353
+ true
354
+ rescue
355
+ false
356
+ end
357
+ end
358
+
359
+ def disconnect! # :nodoc:
360
+ @connection.close rescue nil
361
+ end
362
+
363
+ def reconnect! # :nodoc:
364
+ disconnect!
365
+ @connection = @connection.database.connect(*@connection_params)
366
+ end
367
+
368
+
369
+ # DATABASE STATEMENTS ======================================
370
+
371
+ def select_rows(sql, name = nil)
372
+ select_raw(sql, name).last
373
+ end
374
+
375
+ def execute(sql, name = nil, &block) # :nodoc:
376
+ exec_result = execute_statement(sql, name, &block)
377
+ if exec_result.instance_of?(FireRuby::ResultSet)
378
+ exec_result.close
379
+ exec_result = nil
380
+ end
381
+ return exec_result
382
+ end
383
+
384
+ def begin_db_transaction() # :nodoc:
385
+ @transaction = @connection.start_transaction
386
+ end
387
+
388
+ def commit_db_transaction() # :nodoc:
389
+ @transaction.commit
390
+ ensure
391
+ @transaction = nil
392
+ end
393
+
394
+ def rollback_db_transaction() # :nodoc:
395
+ @transaction.rollback
396
+ ensure
397
+ @transaction = nil
398
+ end
399
+
400
+ def add_limit_offset!(sql, options) # :nodoc:
401
+ if options[:limit]
402
+ limit_string = "FIRST #{options[:limit]}"
403
+ limit_string << " SKIP #{options[:offset]}" if options[:offset]
404
+ sql.sub!(/\A(\s*SELECT\s)/i, '\&' + limit_string + ' ')
405
+ end
406
+ end
407
+
408
+ # Returns the next sequence value from a sequence generator. Not generally
409
+ # called directly; used by ActiveRecord to get the next primary key value
410
+ # when inserting a new database record (see #prefetch_primary_key?).
411
+ def next_sequence_value(sequence_name)
412
+ FireRuby::Generator.new(sequence_name, @connection).next(1)
413
+ end
414
+
415
+ # Inserts the given fixture into the table. Overridden to properly handle blobs.
416
+ def insert_fixture(fixture, table_name)
417
+ super
418
+
419
+ klass = fixture.class_name.constantize rescue nil
420
+ if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
421
+ write_blobs(table_name, klass, fixture)
422
+ end
423
+ end
424
+
425
+ # Writes BLOB values from attributes, as indicated by the BLOB columns of klass.
426
+ def write_blobs(table_name, klass, attributes)
427
+ id = quote(attributes[klass.primary_key])
428
+ klass.columns.select { |col| col.sql_type =~ /BLOB$/i }.each do |col|
429
+ value = attributes[col.name]
430
+ value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
431
+ value = value.read if value.respond_to?(:read)
432
+ next if value.nil? || (value == '')
433
+ s = FireRuby::Statement.new(@connection, @transaction, "UPDATE #{table_name} set #{col.name} = ? WHERE #{klass.primary_key} = #{id}", 3)
434
+ s.execute_for([value.to_s])
435
+ s.close
436
+ end
437
+ end
438
+
439
+
440
+ # SCHEMA STATEMENTS ========================================
441
+
442
+ def current_database # :nodoc:
443
+ file = @connection.database.file.split(':').last
444
+ File.basename(file, '.*')
445
+ end
446
+
447
+ def recreate_database! # :nodoc:
448
+ sql = "SELECT rdb$character_set_name FROM rdb$database"
449
+ charset = select_rows(sql).first[0].rstrip
450
+ disconnect!
451
+ @connection.database.drop(*@connection_params)
452
+ FireRuby::Database.create(@connection.database.file,
453
+ @connection_params[0], @connection_params[1], 4096, charset)
454
+ end
455
+
456
+ def tables(name = nil) # :nodoc:
457
+ sql = "SELECT rdb$relation_name FROM rdb$relations WHERE rdb$system_flag = 0"
458
+ select_rows(sql, name).collect { |row| row[0].rstrip.downcase }
459
+ end
460
+
461
+ def indexes(table_name, name = nil) # :nodoc:
462
+ index_metadata(table_name, false, name).inject([]) do |indexes, row|
463
+ if indexes.empty? or indexes.last.name != row[0]
464
+ indexes << IndexDefinition.new(table_name, row[0].rstrip.downcase, row[1] == 1, [])
465
+ end
466
+ indexes.last.columns << row[2].rstrip.downcase
467
+ indexes
468
+ end
469
+ end
470
+
471
+ def columns(table_name, name = nil) # :nodoc:
472
+ sql = <<-end_sql
473
+ SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
474
+ f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
475
+ COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
476
+ COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
477
+ FROM rdb$relation_fields r
478
+ JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
479
+ WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}'
480
+ ORDER BY r.rdb$field_position
481
+ end_sql
482
+
483
+ select_rows(sql, name).collect do |row|
484
+ field_values = row.collect do |value|
485
+ case value
486
+ when String then value.rstrip
487
+ else value
488
+ end
489
+ end
490
+ FirebirdColumn.new(self, *field_values)
491
+ end
492
+ end
493
+
494
+ def create_table(name, options = {}) # :nodoc:
495
+ begin
496
+ super
497
+ rescue StatementInvalid
498
+ raise unless non_existent_domain_error?
499
+ create_boolean_domain
500
+ super
501
+ end
502
+ unless options[:id] == false or options[:sequence] == false
503
+ sequence_name = options[:sequence] || default_sequence_name(name)
504
+ create_sequence(sequence_name)
505
+ end
506
+ end
507
+
508
+ def drop_table(name, options = {}) # :nodoc:
509
+ super(name)
510
+ unless options[:sequence] == false
511
+ sequence_name = options[:sequence] || default_sequence_name(name)
512
+ drop_sequence(sequence_name) if sequence_exists?(sequence_name)
513
+ end
514
+ end
515
+
516
+ def add_column(table_name, column_name, type, options = {}) # :nodoc:
517
+ super
518
+ rescue StatementInvalid
519
+ raise unless non_existent_domain_error?
520
+ create_boolean_domain
521
+ super
522
+ end
523
+
524
+ def change_column(table_name, column_name, type, options = {}) # :nodoc:
525
+ change_column_type(table_name, column_name, type, options)
526
+ change_column_position(table_name, column_name, options[:position]) if options.include?(:position)
527
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
528
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
529
+ end
530
+
531
+ def change_column_default(table_name, column_name, default) # :nodoc:
532
+ table_name = table_name.to_s.upcase
533
+ sql = <<-end_sql
534
+ UPDATE rdb$relation_fields f1
535
+ SET f1.rdb$default_source =
536
+ (SELECT f2.rdb$default_source FROM rdb$relation_fields f2
537
+ WHERE f2.rdb$relation_name = '#{table_name}'
538
+ AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}'),
539
+ f1.rdb$default_value =
540
+ (SELECT f2.rdb$default_value FROM rdb$relation_fields f2
541
+ WHERE f2.rdb$relation_name = '#{table_name}'
542
+ AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}')
543
+ WHERE f1.rdb$relation_name = '#{table_name}'
544
+ AND f1.rdb$field_name = '#{ar_to_fb_case(column_name.to_s)}'
545
+ end_sql
546
+ transaction do
547
+ add_column(table_name, TEMP_COLUMN_NAME, :string, :default => default)
548
+ execute_statement(sql)
549
+ remove_column(table_name, TEMP_COLUMN_NAME)
550
+ end
551
+ end
552
+
553
+ def change_column_null(table_name, column_name, null, default = nil)
554
+ table_name = table_name.to_s.upcase
555
+ column_name = column_name.to_s.upcase
556
+
557
+ unless null || default.nil?
558
+ execute_statement("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
559
+ end
560
+ execute_statement("UPDATE RDB$RELATION_FIELDS SET RDB$NULL_FLAG = #{null ? 'null' : '1'} WHERE (RDB$FIELD_NAME = '#{column_name}') and (RDB$RELATION_NAME = '#{table_name}')")
561
+ end
562
+
563
+ def rename_column(table_name, column_name, new_column_name) # :nodoc:
564
+ execute_statement("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}")
565
+ end
566
+
567
+ def remove_index(table_name, options) #:nodoc:
568
+ execute_statement("DROP INDEX #{quote_column_name(index_name(table_name, options))}")
569
+ end
570
+
571
+ def rename_table(name, new_name) # :nodoc:
572
+ if table_has_constraints_or_dependencies?(name)
573
+ raise ActiveRecordError,
574
+ "Table #{name} includes constraints or dependencies that are not supported by " <<
575
+ "the Firebird rename_table migration. Try explicitly removing the constraints/" <<
576
+ "dependencies first, or manually renaming the table."
577
+ end
578
+
579
+ transaction do
580
+ copy_table(name, new_name)
581
+ copy_table_indexes(name, new_name)
582
+ end
583
+ begin
584
+ copy_table_data(name, new_name)
585
+ copy_sequence_value(name, new_name)
586
+ rescue
587
+ drop_table(new_name)
588
+ raise
589
+ end
590
+ drop_table(name)
591
+ end
592
+
593
+ def dump_schema_information # :nodoc:
594
+ super << ";\n"
595
+ end
596
+
597
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil) # :nodoc:
598
+ case type
599
+ when :integer then integer_sql_type(limit)
600
+ when :float then float_sql_type(limit)
601
+ when :string then super(type, limit, precision, scale)
602
+ else super(type, limit, precision, scale)
603
+ end
604
+ end
605
+
606
+ private
607
+ def execute_statement(sql, name = nil, &block) # :nodoc:
608
+ @fbe = nil
609
+ log(sql, name) do
610
+ begin
611
+ if @transaction
612
+ @connection.execute(sql, @transaction, &block)
613
+ else
614
+ @connection.execute_immediate(sql, &block)
615
+ end
616
+ rescue Exception => e
617
+ @fbe = e
618
+ raise e
619
+ end
620
+ end
621
+ rescue Exception => se
622
+ def se.nested=value
623
+ @nested=value
624
+ end
625
+ def se.nested
626
+ @nested
627
+ end
628
+ se.nested = @fbe
629
+ raise se
630
+ end
631
+
632
+ def integer_sql_type(limit)
633
+ case limit
634
+ when (1..2) then 'smallint'
635
+ when (3..4) then 'integer'
636
+ else 'bigint'
637
+ end
638
+ end
639
+
640
+ def float_sql_type(limit)
641
+ limit.to_i <= 4 ? 'float' : 'double precision'
642
+ end
643
+
644
+ def select(sql, name = nil)
645
+ fields, rows = select_raw(sql, name)
646
+ result = []
647
+ for row in rows
648
+ row_hash = {}
649
+ fields.each_with_index do |f, i|
650
+ row_hash[f] = row[i]
651
+ end
652
+ result << row_hash
653
+ end
654
+ result
655
+ end
656
+
657
+ def select_raw(sql, name = nil)
658
+ fields = []
659
+ rows = []
660
+ execute_statement(sql, name) do |row|
661
+ array_row = []
662
+ row.each do |column, value|
663
+ fields << fb_to_ar_case(column) if row.number == 1
664
+
665
+ if FireRuby::Blob === value
666
+ temp = value.to_s
667
+ value.close
668
+ value = temp
669
+ end
670
+ array_row << value
671
+ end
672
+ rows << array_row
673
+ end
674
+ return fields, rows
675
+ end
676
+
677
+ def primary_key(table_name)
678
+ if pk_row = index_metadata(table_name, true).to_a.first
679
+ pk_row[2].rstrip.downcase
680
+ end
681
+ end
682
+
683
+ def index_metadata(table_name, pk, name = nil)
684
+ sql = <<-end_sql
685
+ SELECT i.rdb$index_name, i.rdb$unique_flag, s.rdb$field_name
686
+ FROM rdb$indices i
687
+ JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
688
+ LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
689
+ WHERE i.rdb$relation_name = '#{table_name.to_s.upcase}'
690
+ end_sql
691
+ if pk
692
+ sql << "AND c.rdb$constraint_type = 'PRIMARY KEY'\n"
693
+ else
694
+ sql << "AND (c.rdb$constraint_type IS NULL OR c.rdb$constraint_type != 'PRIMARY KEY')\n"
695
+ end
696
+ sql << "ORDER BY i.rdb$index_name, s.rdb$field_position\n"
697
+ execute_statement(sql, name)
698
+ end
699
+
700
+ def change_column_type(table_name, column_name, type, options = {})
701
+ sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit])}"
702
+ execute_statement(sql)
703
+ rescue StatementInvalid
704
+ raise unless non_existent_domain_error?
705
+ create_boolean_domain
706
+ execute_statement(sql)
707
+ end
708
+
709
+ def change_column_position(table_name, column_name, position)
710
+ execute_statement("ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} POSITION #{position}")
711
+ end
712
+
713
+ def copy_table(from, to)
714
+ table_opts = {}
715
+ if pk = primary_key(from)
716
+ table_opts[:primary_key] = pk
717
+ else
718
+ table_opts[:id] = false
719
+ end
720
+ create_table(to, table_opts) do |table|
721
+ from_columns = columns(from).reject { |col| col.name == table_opts[:primary_key] }
722
+ from_columns.each do |column|
723
+ col_opts = [:limit, :default, :null].inject({}) { |opts, opt| opts.merge(opt => column.send(opt)) }
724
+ table.column column.name, column.type, col_opts
725
+ end
726
+ end
727
+ end
728
+
729
+ def copy_table_indexes(from, to)
730
+ indexes(from).each do |index|
731
+ unless index.name[from.to_s]
732
+ raise ActiveRecordError,
733
+ "Cannot rename index #{index.name}, because the index name does not include " <<
734
+ "the original table name (#{from}). Try explicitly removing the index on the " <<
735
+ "original table and re-adding it on the new (renamed) table."
736
+ end
737
+ options = {}
738
+ options[:name] = index.name.gsub(from.to_s, to.to_s)
739
+ options[:unique] = index.unique
740
+ add_index(to, index.columns, options)
741
+ end
742
+ end
743
+
744
+ def copy_table_data(from, to)
745
+ execute_statement("INSERT INTO #{to} SELECT * FROM #{from}", "Copy #{from} data to #{to}")
746
+ end
747
+
748
+ def copy_sequence_value(from, to)
749
+ sequence_value = FireRuby::Generator.new(default_sequence_name(from), @connection).last
750
+ execute_statement("SET GENERATOR #{default_sequence_name(to)} TO #{sequence_value}")
751
+ end
752
+
753
+ def sequence_exists?(sequence_name)
754
+ FireRuby::Generator.exists?(sequence_name, @connection)
755
+ end
756
+
757
+ def create_sequence(sequence_name)
758
+ FireRuby::Generator.create(sequence_name.to_s, @connection)
759
+ end
760
+
761
+ def drop_sequence(sequence_name)
762
+ FireRuby::Generator.new(sequence_name.to_s, @connection).drop
763
+ end
764
+
765
+ def create_boolean_domain
766
+ sql = <<-end_sql
767
+ CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
768
+ CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
769
+ end_sql
770
+ execute_statement(sql) rescue nil
771
+ end
772
+
773
+ def table_has_constraints_or_dependencies?(table_name)
774
+ table_name = table_name.to_s.upcase
775
+ sql = <<-end_sql
776
+ SELECT 1 FROM rdb$relation_constraints
777
+ WHERE rdb$relation_name = '#{table_name}'
778
+ AND rdb$constraint_type IN ('UNIQUE', 'FOREIGN KEY', 'CHECK')
779
+ UNION
780
+ SELECT 1 FROM rdb$dependencies
781
+ WHERE rdb$depended_on_name = '#{table_name}'
782
+ AND rdb$depended_on_type = 0
783
+ end_sql
784
+ !select(sql).empty?
785
+ end
786
+
787
+ def non_existent_domain_error?
788
+ $!.message.include? FireRuby::NON_EXISTENT_DOMAIN_ERROR
789
+ end
790
+
791
+ # Maps uppercase Firebird column names to lowercase for ActiveRecord;
792
+ # mixed-case columns retain their original case.
793
+ def fb_to_ar_case(column_name)
794
+ column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase
795
+ end
796
+
797
+ # Maps lowercase ActiveRecord column names to uppercase for Fierbird;
798
+ # mixed-case columns retain their original case.
799
+ def ar_to_fb_case(column_name)
800
+ column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
801
+ end
802
+ end
803
+ end
804
+ end
805
+