fb_adapter 0.5.5

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,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
+