rubyfb 0.5.2-x86-linux
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.
- data/CHANGELOG +6 -0
- data/LICENSE +411 -0
- data/Manifest +73 -0
- data/README +460 -0
- data/Rakefile +20 -0
- data/examples/example01.rb +65 -0
- data/ext/AddUser.c +464 -0
- data/ext/AddUser.h +37 -0
- data/ext/Backup.c +783 -0
- data/ext/Backup.h +37 -0
- data/ext/Blob.c +421 -0
- data/ext/Blob.h +65 -0
- data/ext/Common.c +54 -0
- data/ext/Common.h +37 -0
- data/ext/Connection.c +863 -0
- data/ext/Connection.h +50 -0
- data/ext/DataArea.c +274 -0
- data/ext/DataArea.h +38 -0
- data/ext/Database.c +449 -0
- data/ext/Database.h +48 -0
- data/ext/FireRuby.c +240 -0
- data/ext/FireRuby.h +50 -0
- data/ext/FireRubyException.c +268 -0
- data/ext/FireRubyException.h +51 -0
- data/ext/Generator.c +689 -0
- data/ext/Generator.h +53 -0
- data/ext/RemoveUser.c +212 -0
- data/ext/RemoveUser.h +37 -0
- data/ext/Restore.c +855 -0
- data/ext/Restore.h +37 -0
- data/ext/ResultSet.c +809 -0
- data/ext/ResultSet.h +60 -0
- data/ext/Row.c +965 -0
- data/ext/Row.h +55 -0
- data/ext/ServiceManager.c +316 -0
- data/ext/ServiceManager.h +48 -0
- data/ext/Services.c +124 -0
- data/ext/Services.h +42 -0
- data/ext/Statement.c +785 -0
- data/ext/Statement.h +62 -0
- data/ext/Transaction.c +684 -0
- data/ext/Transaction.h +50 -0
- data/ext/TypeMap.c +1182 -0
- data/ext/TypeMap.h +51 -0
- data/ext/extconf.rb +28 -0
- data/ext/mkmf.bat +1 -0
- data/lib/SQLType.rb +224 -0
- data/lib/active_record/connection_adapters/rubyfb_adapter.rb +805 -0
- data/lib/mkdoc +1 -0
- data/lib/rubyfb.rb +2 -0
- data/lib/rubyfb_lib.so +0 -0
- data/lib/src.rb +1800 -0
- data/rubyfb.gemspec +31 -0
- data/test/AddRemoveUserTest.rb +56 -0
- data/test/BackupRestoreTest.rb +99 -0
- data/test/BlobTest.rb +57 -0
- data/test/CharacterSetTest.rb +63 -0
- data/test/ConnectionTest.rb +111 -0
- data/test/DDLTest.rb +54 -0
- data/test/DatabaseTest.rb +83 -0
- data/test/GeneratorTest.rb +50 -0
- data/test/KeyTest.rb +140 -0
- data/test/ResultSetTest.rb +162 -0
- data/test/RoleTest.rb +73 -0
- data/test/RowCountTest.rb +65 -0
- data/test/RowTest.rb +203 -0
- data/test/SQLTest.rb +182 -0
- data/test/SQLTypeTest.rb +101 -0
- data/test/ServiceManagerTest.rb +29 -0
- data/test/StatementTest.rb +135 -0
- data/test/TestSetup.rb +11 -0
- data/test/TransactionTest.rb +112 -0
- data/test/TypeTest.rb +92 -0
- data/test/UnitTest.rb +65 -0
- 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
|
+
|