fb_adapter 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ #!/bin/env ruby
2
+ require 'rubygems'
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.author = "Brent Rowland"
6
+ s.name = "fb_adapter"
7
+ s.version = "0.5.5"
8
+ s.date = "2008-02-27"
9
+ s.summary = "ActiveRecord Firebird Adapter"
10
+ s.requirements = "Firebird library fb"
11
+ s.require_path = 'lib'
12
+ s.email = "rowland@rowlandresearch.com"
13
+ s.homepage = "http://www.rowlandresearch.com/ruby/"
14
+ s.rubyforge_project = "fblib"
15
+ s.has_rdoc = false
16
+ # s.extra_rdoc_files = ['README']
17
+ # s.rdoc_options << '--title' << 'Fb -- ActiveRecord Firebird Adapter' << '--main' << 'README' << '-x' << 'test'
18
+ s.files = ['fb_adapter.gemspec'] + Dir.glob('lib/active_record/connection_adapters/*')
19
+ end
20
+
21
+ if __FILE__ == $0
22
+ Gem.manage_gems
23
+ Gem::Builder.new(spec).build
24
+ end
@@ -0,0 +1,446 @@
1
+ # Author: Ken Kunz <kennethkunz@gmail.com>
2
+ # Converted from FireRuby to Fb extension by Brent Rowland <rowland@rowlandresearch.com>
3
+
4
+ require 'active_record/connection_adapters/abstract_adapter'
5
+ require 'base64'
6
+
7
+ module ActiveRecord
8
+ class << Base
9
+ def fb_connection(config) # :nodoc:
10
+ require_library_or_gem 'fb'
11
+ config = config.symbolize_keys.merge(:downcase_names => true)
12
+ unless config.has_key?(:database)
13
+ raise ArgumentError, "No database specified. Missing argument: database."
14
+ end
15
+ config[:database] = File.expand_path(config[:database]) if config[:host] =~ /localhost/i
16
+ config[:database] = "#{config[:host]}:#{config[:database]}" if config[:host]
17
+ db = Fb::Database.new(config)
18
+ begin
19
+ connection = db.connect
20
+ rescue
21
+ require 'pp'; pp config
22
+ connection = config[:create] ? db.create.connect : (raise ConnectionNotEstablished, "No Firebird connections established.")
23
+ end
24
+ ConnectionAdapters::FbAdapter.new(connection, logger, config)
25
+ end
26
+ end
27
+
28
+ module ConnectionAdapters
29
+ class FbColumn < Column # :nodoc:
30
+ def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
31
+ #puts "*** #{type} ~~~ #{sub_type}"
32
+ @firebird_type = Fb::SqlType.from_code(type, sub_type || 0)
33
+ super(name.downcase, nil, @firebird_type, !null_flag)
34
+ @default = parse_default(default_source) if default_source
35
+ @limit = (@firebird_type == 'BLOB') ? 10 * 1024 * 1024 : length
36
+ @domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale
37
+ end
38
+
39
+ def type
40
+ if @domain =~ /BOOLEAN/
41
+ :boolean
42
+ elsif @type == :binary and @sub_type == 1
43
+ :text
44
+ else
45
+ @type
46
+ end
47
+ end
48
+
49
+ # Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
50
+ # This enables Firebird to provide an actual value when context variables are used as column
51
+ # defaults (such as CURRENT_TIMESTAMP).
52
+ def default
53
+ if @default
54
+ sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
55
+ connection = ActiveRecord::Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Fb' }
56
+ if connection
57
+ type_cast connection.select_one(sql)['cast']
58
+ else
59
+ raise ConnectionNotEstablished, "No Firebird connections established."
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.value_to_boolean(value)
65
+ %W(#{FbAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
66
+ end
67
+
68
+ private
69
+ def parse_default(default_source)
70
+ default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i
71
+ return $1 unless $1.upcase == "NULL"
72
+ end
73
+
74
+ def column_def
75
+ case @firebird_type
76
+ when 'CHAR', 'VARCHAR' then "#{@firebird_type}(#{@limit})"
77
+ when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})"
78
+ #when 'DOUBLE' then "DOUBLE PRECISION"
79
+ else @firebird_type
80
+ end
81
+ end
82
+
83
+ def simplified_type(field_type)
84
+ if field_type == 'TIMESTAMP'
85
+ :datetime
86
+ else
87
+ super
88
+ end
89
+ end
90
+ end
91
+
92
+ # The Fb adapter relies on the Fb extension.
93
+ #
94
+ # == Usage Notes
95
+ #
96
+ # === Sequence (Generator) Names
97
+ # The Fb adapter supports the same approach adopted for the Oracle
98
+ # adapter. See ActiveRecord::Base#set_sequence_name for more details.
99
+ #
100
+ # Note that in general there is no need to create a <tt>BEFORE INSERT</tt>
101
+ # trigger corresponding to a Firebird sequence generator when using
102
+ # ActiveRecord. In other words, you don't have to try to make Firebird
103
+ # simulate an <tt>AUTO_INCREMENT</tt> or +IDENTITY+ column. When saving a
104
+ # new record, ActiveRecord pre-fetches the next sequence value for the table
105
+ # and explicitly includes it in the +INSERT+ statement. (Pre-fetching the
106
+ # next primary key value is the only reliable method for the Fb
107
+ # adapter to report back the +id+ after a successful insert.)
108
+ #
109
+ # === BOOLEAN Domain
110
+ # Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily
111
+ # define a +BOOLEAN+ _domain_ for this purpose, e.g.:
112
+ #
113
+ # CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1));
114
+ #
115
+ # When the Fb adapter encounters a column that is based on a domain
116
+ # that includes "BOOLEAN" in the domain name, it will attempt to treat
117
+ # the column as a +BOOLEAN+.
118
+ #
119
+ # By default, the Fb adapter will assume that the BOOLEAN domain is
120
+ # defined as above. This can be modified if needed. For example, if you
121
+ # have a legacy schema with the following +BOOLEAN+ domain defined:
122
+ #
123
+ # CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F'));
124
+ #
125
+ # ...you can add the following line to your <tt>environment.rb</tt> file:
126
+ #
127
+ # ActiveRecord::ConnectionAdapters::Fb.boolean_domain = { :true => 'T', :false => 'F' }
128
+ #
129
+ # === Column Name Case Semantics
130
+ # Firebird and ActiveRecord have somewhat conflicting case semantics for
131
+ # column names.
132
+ #
133
+ # [*Firebird*]
134
+ # The standard practice is to use unquoted column names, which can be
135
+ # thought of as case-insensitive. (In fact, Firebird converts them to
136
+ # uppercase.) Quoted column names (not typically used) are case-sensitive.
137
+ # [*ActiveRecord*]
138
+ # Attribute accessors corresponding to column names are case-sensitive.
139
+ # The defaults for primary key and inheritance columns are lowercase, and
140
+ # in general, people use lowercase attribute names.
141
+ #
142
+ # In order to map between the differing semantics in a way that conforms
143
+ # to common usage for both Firebird and ActiveRecord, uppercase column names
144
+ # in Firebird are converted to lowercase attribute names in ActiveRecord,
145
+ # and vice-versa. Mixed-case column names retain their case in both
146
+ # directions. Lowercase (quoted) Firebird column names are not supported.
147
+ # This is similar to the solutions adopted by other adapters.
148
+ #
149
+ # In general, the best approach is to use unquoted (case-insensitive) column
150
+ # names in your Firebird DDL (or if you must quote, use uppercase column
151
+ # names). These will correspond to lowercase attributes in ActiveRecord.
152
+ #
153
+ # For example, a Firebird table based on the following DDL:
154
+ #
155
+ # CREATE TABLE products (
156
+ # id BIGINT NOT NULL PRIMARY KEY,
157
+ # "TYPE" VARCHAR(50),
158
+ # name VARCHAR(255) );
159
+ #
160
+ # ...will correspond to an ActiveRecord model class called +Product+ with
161
+ # the following attributes: +id+, +type+, +name+.
162
+ #
163
+ # ==== Quoting <tt>"TYPE"</tt> and other Firebird reserved words:
164
+ # In ActiveRecord, the default inheritance column name is +type+. The word
165
+ # _type_ is a Firebird reserved word, so it must be quoted in any Firebird
166
+ # SQL statements. Because of the case mapping described above, you should
167
+ # always reference this column using quoted-uppercase syntax
168
+ # (<tt>"TYPE"</tt>) within Firebird DDL or other SQL statements (as in the
169
+ # example above). This holds true for any other Firebird reserved words used
170
+ # as column names as well.
171
+ #
172
+ # === Migrations
173
+ # The Fb adapter does not currently support Migrations.
174
+ #
175
+ # == Connection Options
176
+ # The following options are supported by the Fb adapter.
177
+ #
178
+ # <tt>:database</tt>::
179
+ # <i>Required option.</i> Specifies one of: (i) a Firebird database alias;
180
+ # (ii) the full path of a database file; _or_ (iii) a full Firebird
181
+ # connection string. <i>Do not specify <tt>:host</tt>, <tt>:service</tt>
182
+ # or <tt>:port</tt> as separate options when using a full connection
183
+ # string.</i>
184
+ # <tt>:username</tt>::
185
+ # Specifies the database user. Defaults to 'sysdba'.
186
+ # <tt>:password</tt>::
187
+ # Specifies the database password. Defaults to 'masterkey'.
188
+ # <tt>:charset</tt>::
189
+ # Specifies the character set to be used by the connection. Refer to the
190
+ # Firebird documentation for valid options.
191
+ class FbAdapter < AbstractAdapter
192
+ @@boolean_domain = { :true => 1, :false => 0 }
193
+ cattr_accessor :boolean_domain
194
+
195
+ def initialize(connection, logger, connection_params=nil)
196
+ super(connection, logger)
197
+ @connection_params = connection_params
198
+ end
199
+
200
+ def adapter_name # :nodoc:
201
+ 'Fb'
202
+ end
203
+
204
+ # Returns true for Fb adapter (since Firebird requires primary key
205
+ # values to be pre-fetched before insert). See also #next_sequence_value.
206
+ def prefetch_primary_key?(table_name = nil)
207
+ true
208
+ end
209
+
210
+ def default_sequence_name(table_name, primary_key) # :nodoc:
211
+ "#{table_name}_seq"
212
+ end
213
+
214
+
215
+ # QUOTING ==================================================
216
+
217
+ def quote(value, column = nil) # :nodoc:
218
+ case value
219
+ when String
220
+ "@#{Base64.encode64(value).chop}@"
221
+ when Float, Fixnum, Bignum then quote_number(value)
222
+ when Date then quote_date(value)
223
+ when Time, DateTime then quote_timestamp(value)
224
+ when NilClass then "NULL"
225
+ when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
226
+ when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
227
+ else quote_object(value)
228
+ end
229
+ end
230
+
231
+ def quote_number(value)
232
+ # "@#{Base64.encode64(value.to_s).chop}@"
233
+ value.to_s
234
+ end
235
+
236
+ def quote_date(value)
237
+ "@#{Base64.encode64(value.strftime('%Y-%m-%d')).chop}@"
238
+ end
239
+
240
+ def quote_timestamp(value)
241
+ "@#{Base64.encode64(value.strftime('%Y-%m-%d %H:%M:%S')).chop}@"
242
+ end
243
+
244
+ def quote_string(string) # :nodoc:
245
+ string.gsub(/'/, "''")
246
+ end
247
+
248
+ def quote_object(obj)
249
+ return obj.respond_to?(:quoted_id) ? obj.quoted_id : "@#{Base64.encode64(obj.to_yaml).chop}@"
250
+ end
251
+
252
+ def quote_column_name(column_name) # :nodoc:
253
+ %Q("#{ar_to_fb_case(column_name.to_s)}")
254
+ end
255
+
256
+ def quoted_true # :nodoc:
257
+ quote(boolean_domain[:true])
258
+ end
259
+
260
+ def quoted_false # :nodoc:
261
+ quote(boolean_domain[:false])
262
+ end
263
+
264
+
265
+ # CONNECTION MANAGEMENT ====================================
266
+
267
+ def active?
268
+ @connection.open?
269
+ end
270
+
271
+ def disconnect!
272
+ @connection.close rescue nil
273
+ end
274
+
275
+ def reconnect!
276
+ disconnect!
277
+ @connection = Fb::Database.connect(@connection_params)
278
+ end
279
+
280
+ # DATABASE STATEMENTS ======================================
281
+
282
+ def translate(sql)
283
+ sql.gsub!(/\bIN\s+\(NULL\)/i, 'IS NULL')
284
+ sql.sub!(/\bWHERE\s.*$/im) do |m|
285
+ m.gsub(/\s=\s*NULL\b/i, ' IS NULL')
286
+ end
287
+ sql.gsub!(/\sIN\s+\([^\)]*\)/mi) do |m|
288
+ m.gsub(/\(([^\)]*)\)/m) { |n| n.gsub(/\@(.*?)\@/m) { |n| "'#{quote_string(Base64.decode64(n[1..-1]))}'" } }
289
+ end
290
+ args = []
291
+ sql.gsub!(/\@(.*?)\@/m) { |m| args << Base64.decode64(m[1..-1]); '?' }
292
+ yield(sql, args) if block_given?
293
+ end
294
+
295
+ def expand(sql, args)
296
+ sql + ', ' + args * ', '
297
+ end
298
+
299
+ def log(sql, args, name, &block)
300
+ super(expand(sql, args), name, &block)
301
+ end
302
+
303
+ def select_all(sql, name = nil, format = :hash) # :nodoc:
304
+ translate(sql) do |sql, args|
305
+ log(sql, args, name) do
306
+ @connection.query(format, sql, *args)
307
+ end
308
+ end
309
+ end
310
+
311
+ def select_one(sql, name = nil, format = :hash) # :nodoc:
312
+ translate(sql) do |sql, args|
313
+ log(sql, args, name) do
314
+ @connection.query(format, sql, *args).first
315
+ end
316
+ end
317
+ end
318
+
319
+ def execute(sql, name = nil, &block) # :nodoc:
320
+ translate(sql) do |sql, args|
321
+ log(sql, args, name) do
322
+ @connection.execute(sql, *args, &block)
323
+ end
324
+ end
325
+ end
326
+
327
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # :nodoc:
328
+ execute(sql, name)
329
+ id_value
330
+ end
331
+
332
+ alias_method :update, :execute
333
+ alias_method :delete, :execute
334
+
335
+ def begin_db_transaction() # :nodoc:
336
+ @transaction = @connection.transaction('READ COMMITTED')
337
+ end
338
+
339
+ def commit_db_transaction() # :nodoc:
340
+ @transaction = @connection.commit
341
+ end
342
+
343
+ def rollback_db_transaction() # :nodoc:
344
+ @transaction = @connection.rollback
345
+ end
346
+
347
+ def add_lock!(sql, options) # :nodoc:
348
+ sql
349
+ end
350
+
351
+ def add_limit_offset!(sql, options) # :nodoc:
352
+ if options[:limit]
353
+ limit_string = "FIRST #{options[:limit]}"
354
+ limit_string << " SKIP #{options[:offset]}" if options[:offset]
355
+ sql.sub!(/\A(\s*SELECT\s)/i, '\&' + limit_string + ' ')
356
+ end
357
+ end
358
+
359
+ # Returns the next sequence value from a sequence generator. Not generally
360
+ # called directly; used by ActiveRecord to get the next primary key value
361
+ # when inserting a new database record (see #prefetch_primary_key?).
362
+ def next_sequence_value(sequence_name)
363
+ select_one("SELECT GEN_ID(#{sequence_name}, 1) FROM RDB$DATABASE", nil, :array).first
364
+ end
365
+
366
+ # SCHEMA STATEMENTS ========================================
367
+
368
+ def columns(table_name, name = nil) # :nodoc:
369
+ sql = <<-END_SQL
370
+ SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
371
+ f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
372
+ COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
373
+ COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
374
+ FROM rdb$relation_fields r
375
+ JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
376
+ WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}'
377
+ ORDER BY r.rdb$field_position
378
+ END_SQL
379
+ select_all(sql, name, :array).collect do |field|
380
+ field_values = field.collect do |value|
381
+ case value
382
+ when String then value.rstrip
383
+ else value
384
+ end
385
+ end
386
+ FbColumn.new(*field_values)
387
+ end
388
+ end
389
+
390
+ def tables(name = nil)
391
+ @connection.table_names.map {|t| t.downcase }
392
+ end
393
+
394
+ def indexes(table_name, name = nil) #:nodoc:
395
+ result = @connection.indexes.values.select {|ix| ix.table_name == table_name && ix.index_name !~ /^rdb\$/ }
396
+ indexes = result.map {|ix| IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns) }
397
+ indexes
398
+ end
399
+
400
+ def table_alias_length
401
+ 255
402
+ end
403
+
404
+ def rename_column(table_name, column_name, new_column_name)
405
+ execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TO #{new_column_name}"
406
+ end
407
+
408
+ def remove_index(table_name, options = {})
409
+ execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
410
+ end
411
+
412
+ def supports_migrations?
413
+ false
414
+ end
415
+
416
+ def native_database_types
417
+ {
418
+ :primary_key => "integer not null primary key",
419
+ :string => { :name => "varchar", :limit => 255 },
420
+ :text => { :name => "blob sub_type text" },
421
+ :integer => { :name => "integer" },
422
+ :float => { :name => "float" },
423
+ :datetime => { :name => "timestamp" },
424
+ :timestamp => { :name => "timestamp" },
425
+ :time => { :name => "time" },
426
+ :date => { :name => "date" },
427
+ :binary => { :name => "blob" },
428
+ :boolean => { :name => "integer" }
429
+ }
430
+ end
431
+
432
+ private
433
+ # Maps uppercase Firebird column names to lowercase for ActiveRecord;
434
+ # mixed-case columns retain their original case.
435
+ def fb_to_ar_case(column_name)
436
+ column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase
437
+ end
438
+
439
+ # Maps lowercase ActiveRecord column names to uppercase for Fierbird;
440
+ # mixed-case columns retain their original case.
441
+ def ar_to_fb_case(column_name)
442
+ column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
443
+ end
444
+ end
445
+ end
446
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fb_adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.5
5
+ platform: ruby
6
+ authors:
7
+ - Brent Rowland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-02-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: rowland@rowlandresearch.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - fb_adapter.gemspec
26
+ - lib/active_record/connection_adapters/fb_adapter.rb
27
+ has_rdoc: false
28
+ homepage: http://www.rowlandresearch.com/ruby/
29
+ post_install_message:
30
+ rdoc_options: []
31
+
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: "0"
39
+ version:
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ requirements:
47
+ - Firebird library fb
48
+ rubyforge_project: fblib
49
+ rubygems_version: 1.0.1
50
+ signing_key:
51
+ specification_version: 2
52
+ summary: ActiveRecord Firebird Adapter
53
+ test_files: []
54
+