activerecord-mimer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2006 Fredrik Ålund <fredrik.alund@mimer.se>
2
+ Copyright (c) 2006 Ola Bini <ola@ologix.com>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,70 @@
1
+ = ActiveRecord Mimer
2
+
3
+ This project is basically an ActiveRecord connector for the Mimer SQL
4
+ database. It uses ODBC to communicate with the database, and supports
5
+ most regular operations and migrations.
6
+
7
+ == Dependencies
8
+
9
+ unixODBC 2.2.8 or later
10
+ A good installation instruction can be found
11
+ at http://developer.mimer.com/howto/howto_57.htm
12
+
13
+ Ruby-ODBC
14
+ http://www.ch-werner.de/rubyodbc/
15
+
16
+ == Using ActiveRecord Mimer
17
+
18
+ === Standalone, with ActiveRecord
19
+
20
+ Using this adapter is very simple, but requires that you manually add the
21
+ adapter. So, to use it in a script, add this to the requires':
22
+ RAILS_CONNECTION_ADAPTERS = ['mimer']
23
+ require 'active_record'
24
+
25
+ After this you can establish a JDBC connection like this:
26
+
27
+ ActiveRecord::Base.establish_connection(
28
+ :adapter => 'mimer',
29
+ :dsn => 'yourODBCdsn',
30
+ :username => 'username',
31
+ :password => 'pwd'
32
+ )
33
+
34
+ If provided, password and username will be used. After the connection is established
35
+ Active Record can be used as usual.
36
+
37
+ === Inside Rails
38
+
39
+ Using the adapter inside Rails is slightly more complicated, since we
40
+ don't have control over the load process. The easiest way to add
41
+ Mimer support is to manually edit your environment.rb, and add this line:
42
+
43
+ require 'mimer_adapter'
44
+
45
+ between
46
+
47
+ require File.join(File.dirname(__FILE__), 'boot')
48
+
49
+ and
50
+
51
+ Rails::Initializer.run do |config|
52
+
53
+ The head of my environment.rb after these modifications look like this:
54
+
55
+ RAILS_GEM_VERSION = '1.1.6'
56
+
57
+ # Bootstrap the Rails environment, frameworks, and default configuration
58
+ require File.join(File.dirname(__FILE__), 'boot')
59
+
60
+ require 'mimer_adapter'
61
+
62
+ Rails::Initializer.run do |config|
63
+ # Settings in config/environments/* take precedence those specified here
64
+
65
+ == Authors
66
+ This project was written by:
67
+ Fredrik Ålund and Ola Bini with some code from rails-odbc.
68
+
69
+ == License
70
+ ActiveRecord-Mimer is released under an MIT license.
@@ -0,0 +1,783 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'active_record/connection_adapters/abstract_adapter'
5
+
6
+ begin
7
+ require_library_or_gem 'odbc' unless self.class.const_defined?(:ODBC)
8
+
9
+ module ActiveRecord
10
+ class Base
11
+ # Establishes a connection to the database that's used by all Active Record objects
12
+ def self.mimer_connection(config) # :nodoc:
13
+ config = config.symbolize_keys
14
+ if config.has_key?(:dsn)
15
+ dsn = config[:dsn]
16
+ else
17
+ raise ActiveRecordError, "No data source name (DSN) specified."
18
+ end
19
+ username = config[:username] ? config[:username].to_s : nil
20
+ password = config[:password] ? config[:password].to_s : nil
21
+ conn = ODBC::connect(dsn, username, password)
22
+ conn.autocommit = true
23
+ ConnectionAdapters::MimerAdapter.new(conn, [dsn, username, password],logger)
24
+ end
25
+ end
26
+
27
+ module ConnectionAdapters
28
+ class MimerAdapter < AbstractAdapter
29
+ SQL_NO_NULLS = 0 # :nodoc:
30
+ SQL_NULLABLE = 1 # :nodoc:
31
+ SQL_NULLABLE_UNKNOWN = 2 # :nodoc:
32
+
33
+ def initialize(connection, connection_options, logger = nil)
34
+ super(connection, logger)
35
+ @connection, @connection_options = connection, connection_options
36
+ end
37
+
38
+ def adapter_name; 'Mimer'; end
39
+
40
+ def supports_migrations?
41
+ true
42
+ end
43
+
44
+ # Does the database support COUNT(DISTINCT) queries?
45
+ # e.g. <tt>select COUNT(DISTINCT ArtistID) from CDs</tt>
46
+ def supports_count_distinct?
47
+ true
48
+ end
49
+
50
+ # Should primary key values be selected from their corresponding
51
+ # sequence before the insert statement? If true, #next_sequence_value
52
+ # is called before each insert to set the record's primary key.
53
+ def prefetch_primary_key?(table_name = nil)
54
+ true
55
+ end
56
+
57
+ def next_sequence_value(sequence_name)
58
+ select_one("select next_value of #{sequence_name} AS id from system.onerow")['id']
59
+ end
60
+
61
+ # Returns true if this connection active.
62
+ def active?
63
+ @connection.connected?
64
+ end
65
+
66
+ # Reconnects to the database.
67
+ def reconnect!
68
+ @connection.disconnect if @connection.connected?
69
+ @connection = ODBC::connect(*@connection_options)
70
+ rescue Exception => e
71
+ raise ActiveRecordError, e.message
72
+ end
73
+
74
+ # Disconnects from the database.
75
+ def disconnect!
76
+ @connection.disconnect if @connection.connected?
77
+ rescue Exception => e
78
+ raise ActiveRecordError, e.message
79
+ end
80
+
81
+ def quote(value, column = nil)
82
+ case value
83
+ when String
84
+ if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
85
+ "#{column.class.string_to_binary(value)}"
86
+ elsif (column && [:integer, :float].include?(column.type)) ||
87
+ (column.nil? &&
88
+ (value =~ /^[-+]?[0-9]+[.]?[0-9]*([eE][-+]?[0-9]+)?$/))
89
+ value
90
+ else
91
+ "'#{quote_string(value)}'" # ' (for ruby-mode)
92
+ end
93
+ when NilClass then "NULL"
94
+ when TrueClass then (column && column.type == :integer ?
95
+ '1' : quoted_true)
96
+ when FalseClass then (column && column.type == :integer ?
97
+ '0' : quoted_false)
98
+ when Float, Fixnum, Bignum then value.to_s
99
+ when Date then quoted_date(value)
100
+ when Time
101
+ if column.type == :time
102
+ quoted_time(value)
103
+ else
104
+ quoted_date(value)
105
+ end
106
+ when DateTime
107
+ quoted_date(value)
108
+ else
109
+ super
110
+ end
111
+ rescue Exception => e
112
+ raise ActiveRecordError, e.message
113
+ end
114
+
115
+ # Quotes a string, escaping any ' (single quote) and \ (backslash)
116
+ # characters.
117
+ def quote_string(string)
118
+ string.gsub(/\'/, "''")
119
+ end
120
+
121
+ # Returns a quoted form of the column name.
122
+ def quote_column_name(name)
123
+ name = name.to_s if name.class == Symbol
124
+ name
125
+ end
126
+
127
+ def quoted_true
128
+ '1'
129
+ end
130
+
131
+ def quoted_false
132
+ '0'
133
+ end
134
+
135
+ def quoted_date(value)
136
+ # Ideally, we'd return an ODBC date or timestamp literal escape
137
+ # sequence, but not all ODBC drivers support them.
138
+ case value
139
+ when Time, DateTime
140
+ %Q!TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'!
141
+ when Date
142
+ %Q!DATE '#{value.strftime("%Y-%m-%d")}'!
143
+ end
144
+ end
145
+
146
+ def quoted_time(value)
147
+ %Q!TIME '#{value.strftime("%H:%M:%S")}'!
148
+ end
149
+
150
+ # Begins a transaction (and turns off auto-committing).
151
+ def begin_db_transaction
152
+ @connection.autocommit = false
153
+ rescue Exception => e
154
+ raise ActiveRecordError, e.message
155
+ end
156
+
157
+ # Commits the transaction (and turns on auto-committing).
158
+ def commit_db_transaction
159
+ @connection.commit
160
+ # ODBC chains transactions. Turn autocommit on after commit to
161
+ # allow explicit transaction initiation.
162
+ @connection.autocommit = true
163
+ rescue Exception => e
164
+ raise ActiveRecordError, e.message
165
+ end
166
+
167
+ # Rolls back the transaction (and turns on auto-committing).
168
+ def rollback_db_transaction
169
+ @connection.rollback
170
+ # ODBC chains transactions. Turn autocommit on after rollback to
171
+ # allow explicit transaction initiation.
172
+ @connection.autocommit = true
173
+ rescue Exception => e
174
+ raise ActiveRecordError, e.message
175
+ end
176
+
177
+ # Appends +LIMIT+ and/or +OFFSET+ options to a SQL statement.
178
+ # See DatabaseStatements#add_limit_offset!
179
+ #--
180
+ # Base class accepts only +LIMIT+ *AND* +OFFSET+
181
+ def add_limit_offset!(sql, options)
182
+ if limit = options[:limit] then sql << " LIMIT #{limit}" end
183
+ if offset = options[:offset] then sql << " OFFSET #{offset}" end
184
+ end
185
+
186
+ # Returns an array of record hashes with the column names as keys and
187
+ # column values as values.
188
+ def select_all(sql, name = nil)
189
+ retVal = []
190
+ scrollableCursor = false
191
+ limit = 0
192
+ offset = 0
193
+ qry = sql.dup
194
+
195
+ # Strip OFFSET and LIMIT from query if present, since ODBC doesn't
196
+ # support them in a generic form.
197
+ if qry =~ /(\bLIMIT\s+)(\d+)/i then
198
+ if (limit = $2.to_i) == 0 then return retVal end
199
+ end
200
+
201
+ if qry =~ /(\bOFFSET\s+)(\d+)/i then offset = $2.to_i end
202
+ qry.gsub!(/(\bLIMIT\s+\d+|\bOFFSET\s+\d+)/i, '')
203
+
204
+ # It's been assumed that it's quicker to support an offset and/or
205
+ # limit restriction using a forward-only cursor. A static cursor will
206
+ # presumably take a snapshot of the whole result set, whereas when
207
+ # using a forward-only cursor we only fetch the first offset+limit
208
+ # rows.
209
+ # Execute the query
210
+ begin
211
+ stmt = @connection.run(qry)
212
+ rescue Exception => e
213
+ stmt.drop unless stmt.nil?
214
+ raise StatementInvalid, e.message
215
+ end
216
+
217
+ rColDescs = stmt.columns(true)
218
+
219
+ # Get the rows, handling any offset and/or limit stipulated
220
+ if scrollableCursor then
221
+ rRows = nil
222
+ # scrollableCursor == true => offset > 0
223
+ if stmt.fetch_scroll(ODBC::SQL_FETCH_ABSOLUTE, offset)
224
+ rRows = limit > 0 ? stmt.fetch_many(limit) : stmt.fetch_all
225
+ end
226
+ else
227
+ rRows = limit > 0 ? stmt.fetch_many(offset + limit) : stmt.fetch_all
228
+ # Enforce OFFSET
229
+ if offset > 0 then
230
+ if rRows && rRows.length > offset then
231
+ rRows.slice!(0, offset)
232
+ else
233
+ rRows = nil
234
+ end
235
+ end
236
+ # Enforce LIMIT
237
+ if limit > 0 && rRows && rRows.length > limit then
238
+ rRows.slice!(limit..(rRows.length-1))
239
+ end
240
+ end
241
+
242
+ # Convert rows from arrays to hashes
243
+ if rRows
244
+ rRows.each do |row|
245
+ h = Hash.new
246
+ (0...row.length).each do |iCol|
247
+ h[activeRecIdentCase(rColDescs[iCol].name)] =
248
+ convertOdbcValToGenericVal(row[iCol])
249
+ end
250
+ retVal << h
251
+ end
252
+ end
253
+
254
+ stmt.drop
255
+ retVal
256
+ end
257
+
258
+ # Returns a record hash with the column names as keys and column values
259
+ # as values.
260
+ def select_one(sql, name = nil)
261
+ retVal = nil
262
+ scrollableCursor = false
263
+ offset = 0
264
+ qry = sql.dup
265
+
266
+ # Strip OFFSET and LIMIT from query if present, since ODBC doesn't
267
+ # support them in a generic form.
268
+ if qry =~ /(\bLIMIT\s+)(\d+)/i then
269
+ # Check for 'LIMIT 0' otherwise ignore LIMIT
270
+ if $2.to_i == 0 then return retVal end
271
+ end
272
+
273
+ if qry =~ /(\bOFFSET\s+)(\d+)/i then offset = $2.to_i end
274
+ qry.gsub!(/(\bLIMIT\s+\d+|\bOFFSET\s+\d+)/i, '')
275
+
276
+ # It's been assumed that it's quicker to support an offset
277
+ # restriction using a forward-only cursor. A static cursor will
278
+ # presumably take a snapshot of the whole result set, whereas when
279
+ # using a forward-only cursor we only fetch the first offset+1
280
+ # rows.
281
+ # Execute the query
282
+ begin
283
+ stmt = @connection.run(qry)
284
+ rescue Exception => e
285
+ stmt.drop unless stmt.nil?
286
+ raise StatementInvalid, e.message
287
+ end
288
+
289
+ # Get one row, handling any offset stipulated
290
+ rColDescs = stmt.columns(true)
291
+ if scrollableCursor then
292
+ # scrollableCursor == true => offset > 0
293
+ stmt.fetch_scroll(ODBC::SQL_FETCH_ABSOLUTE, offset)
294
+ row = stmt.fetch
295
+ else
296
+ row = nil
297
+ rRows = stmt.fetch_many(offset + 1)
298
+ if rRows && rRows.length > offset then
299
+ row = rRows[offset]
300
+ end
301
+ end
302
+
303
+ # Convert row from array to hash
304
+ if row then
305
+ retVal = h = Hash.new
306
+ (0...row.length).each do |iCol|
307
+ h[activeRecIdentCase(rColDescs[iCol].name)] =
308
+ convertOdbcValToGenericVal(row[iCol])
309
+ end
310
+ end
311
+
312
+ stmt.drop
313
+ retVal
314
+ end
315
+
316
+ # Executes the SQL statement in the context of this connection.
317
+ # Returns the number of rows affected.
318
+ def execute(sql, name = nil)
319
+ begin
320
+ @connection.do(sql)
321
+ rescue Exception => e
322
+ raise StatementInvalid, e.message
323
+ end
324
+ end
325
+
326
+ alias_method :delete, :execute
327
+ alias_method :update, :execute
328
+
329
+ # Returns the ID of the last inserted row.
330
+ def insert(sql, name = nil, pk = nil, id_value = nil,
331
+ sequence_name = nil)
332
+ # id_value ::= pre-assigned id
333
+ retry_count = 0
334
+ begin
335
+ pre_insert(sql, name, pk, id_value, sequence_name) if respond_to?("pre_insert")
336
+ stmt = @connection.run(sql)
337
+ table = sql.split(" ", 4)[2]
338
+ res = id_value || last_insert_id(table, sequence_name ||
339
+ default_sequence_name(table, pk), stmt)
340
+ rescue Exception => e
341
+ raise StatementInvalid, e.message
342
+ ensure
343
+ post_insert(sql, name, pk, id_value, sequence_name) if respond_to?("post_insert")
344
+ stmt.drop unless stmt.nil?
345
+ end
346
+ res
347
+ end
348
+
349
+ # Returns the default sequence name for a table.
350
+ def default_sequence_name(table, primary_key=nil)
351
+ "#{table}_seq"
352
+ end
353
+
354
+ # Set the sequence to the max value of the table's column.
355
+ def reset_sequence!(table, column, sequence = nil)
356
+ super(table, column, sequence)
357
+ rescue Exception => e
358
+ raise ActiveRecordError, e.message
359
+ end
360
+
361
+ def table_alias_length
362
+ maxIdentLen = @connection.get_info(ODBC::SQL_MAX_IDENTIFIER_LEN)
363
+ maxTblNameLen = @connection.get_info(ODBC::SQL_MAX_TABLE_NAME_LEN)
364
+ maxTblNameLen < maxIdentLen ? maxTblNameLen : maxIdentLen
365
+ end
366
+
367
+ # Returns an array of table names, for database tables visible on the
368
+ # current connection.
369
+ def tables(name = nil)
370
+ tblNames = []
371
+ currentUser = @connection.get_info(ODBC::SQL_USER_NAME)
372
+ stmt = @connection.tables
373
+ resultSet = stmt.fetch_all || []
374
+ resultSet.each do |row|
375
+ tblNames << activeRecIdentCase(row[2]) if row[1].casecmp(currentUser) == 0
376
+ end
377
+ stmt.drop
378
+ tblNames
379
+ rescue Exception => e
380
+ raise ActiveRecordError, e.message
381
+ end
382
+
383
+ # Returns an array of Column objects for the table specified by +table_name+.
384
+ def columns(table_name, name = nil)
385
+ table_name = table_name.to_s if table_name.class == Symbol
386
+
387
+ getDbTypeInfo
388
+ cols = []
389
+ stmt = @connection.columns(dbmsIdentCase(table_name))
390
+ resultSet = stmt.fetch_all || []
391
+ resultSet.each do |col|
392
+ colName = col[3] # SQLColumns: COLUMN_NAME
393
+ colDefault = col[12] # SQLColumns: COLUMN_DEF
394
+ colSqlType = col[4] # SQLColumns: DATA_TYPE
395
+ colNativeType = col[5] # SQLColumns: TYPE_NAME
396
+ colLimit = col[6] # SQLColumns: COLUMN_SIZE
397
+
398
+ odbcIsNullable = col[17] # SQLColumns: IS_NULLABLE
399
+ odbcNullable = col[10] # SQLColumns: NULLABLE
400
+ # isNotNullable == true => *definitely not* nullable
401
+ # == false => *may* be nullable
402
+ isNotNullable = (odbcIsNullable.match('NO') != nil)
403
+ # Assume column is nullable if odbcNullable == SQL_NULLABLE_UNKNOWN
404
+ colNullable = !(isNotNullable || odbcNullable == SQL_NO_NULLS)
405
+
406
+ # SQL Server ODBC drivers may wrap default value in parentheses
407
+ if colDefault =~ /^\('(.*)'\)$/ # SQL Server character default
408
+ colDefault = $1
409
+ elsif colDefault =~ /^\((.*)\)$/ # SQL Server numeric default
410
+ colDefault = $1
411
+ # ODBC drivers should return string column defaults in quotes
412
+ # Oracle also includes a trailing space.
413
+ elsif colDefault =~ /^'(.*)' *$/
414
+ colDefault = $1
415
+ end
416
+ cols << MimerColumn.new(activeRecIdentCase(colName), table_name,
417
+ colDefault, colSqlType, colNativeType, colNullable, colLimit,@typeInfo)
418
+ end
419
+ stmt.drop
420
+ cols
421
+ rescue Exception => e
422
+ raise ActiveRecordError, e.message
423
+ end
424
+
425
+ # Returns an array of indexes for the given table.
426
+ def indexes(table_name, name = nil)
427
+ indexes = []
428
+ indexCols = indexName = isUnique = nil
429
+
430
+ stmt = @connection.indexes(dbmsIdentCase(table_name))
431
+ rs = stmt.fetch_all || []
432
+ rs.each_index do |iRow|
433
+ row = rs[iRow]
434
+
435
+ # Skip table statistics
436
+ next if row[6] == 0 # SQLStatistics: TYPE
437
+
438
+ if (row[7] == 1) # SQLStatistics: ORDINAL_POSITION
439
+ # Start of column descriptor block for next index
440
+ indexCols = Array.new
441
+ isUnique = (row[3] == 0) # SQLStatistics: NON_UNIQUE
442
+ indexName = String.new(row[5]) # SQLStatistics: INDEX_NAME
443
+ end
444
+ indexCols << activeRecIdentCase(row[8]) # SQLStatistics: COLUMN_NAME
445
+
446
+ lastRow = (iRow == rs.length - 1)
447
+ if lastRow
448
+ lastColOfIndex = true
449
+ else
450
+ nextRow = rs[iRow + 1]
451
+ lastColOfIndex = (nextRow[6] == 0 || nextRow[7] == 1)
452
+ end
453
+
454
+ if lastColOfIndex
455
+ indexes << IndexDefinition.new(table_name,
456
+ activeRecIdentCase(indexName), isUnique, indexCols)
457
+ end
458
+ end
459
+ indexes
460
+ rescue Exception => e
461
+ raise ActiveRecordError, e.message
462
+ ensure
463
+ stmt.drop unless stmt.nil?
464
+ end
465
+
466
+ # Returns a Hash of mappings from Rails' abstract data types to the
467
+ # native database types.
468
+ # See TableDefinition#column for details of the abstract data types.
469
+ def native_database_types
470
+ return {}.merge(@abstract2NativeTypeMap) unless @abstract2NativeTypeMap.nil?
471
+ @abstract2NativeTypeMap =
472
+ {
473
+ :primary_key => "integer not null primary key",
474
+ :string => {:name => "VARCHAR", :limit => 255},
475
+ :text => {:name => "CLOB"},
476
+ :integer => {:name => "INTEGER"},
477
+ :float => {:name => "FLOAT"},
478
+ :datetime => {:name => "TIMESTAMP"},
479
+ :timestamp => {:name => "TIMESTAMP"},
480
+ :time => {:name => "TIME"},
481
+ :date => {:name => "DATE"},
482
+ :binary => {:name => "BLOB"},
483
+ :boolean => {:name => "SMALLINT"}
484
+ }
485
+
486
+ {}.merge(@abstract2NativeTypeMap)
487
+ rescue Exception => e
488
+ raise ActiveRecordError, e.message
489
+ end
490
+
491
+ def add_column(table_name, column_name, type, options = {})
492
+ super(table_name, column_name, type, options)
493
+ rescue Exception => e
494
+ raise ActiveRecordError, e.message
495
+ end
496
+
497
+ def remove_column(table_name, column_name)
498
+ execute "ALTER TABLE #{table_name} DROP COLUMN #{quote_column_name(column_name)}"
499
+ end
500
+
501
+ def change_column(table_name, column_name, type, options = {})
502
+ execute "ALTER TABLE #{table_name} ALTER #{column_name} #{type_to_sql(type,options[:limit])}"
503
+ change_column_default(table_name, column_name, options[:default]) unless options[:default].nil?
504
+ end
505
+
506
+ def change_column_default(table_name, column_name, default)
507
+ execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{quote(default)}"
508
+ end
509
+
510
+ def remove_index(table_name, options = {})
511
+ execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
512
+ end
513
+
514
+ def create_table(name, options = {})
515
+ super(name, options)
516
+ execute "CREATE UNIQUE SEQUENCE #{name}_seq" unless options[:id] == false
517
+ end
518
+
519
+ def drop_table(name)
520
+ super(name)
521
+ execute "DROP SEQUENCE #{name}_seq"
522
+ rescue Exception => e
523
+ if e.message !~ /ORA-02289/i
524
+ raise
525
+ end
526
+ end
527
+
528
+ def type_to_sql(type, limit = nil) # :nodoc:
529
+ native = native_database_types[type]
530
+ column_type_sql = String.new(type == :primary_key ? native : native[:name])
531
+ # if there's no limit in the type definition, assume that the type
532
+ # doesn't support a length qualifier
533
+ column_type_sql << "(#{limit || native[:limit]})" if native[:limit]
534
+ column_type_sql
535
+ rescue Exception => e
536
+ raise ActiveRecordError, e.message
537
+ end
538
+
539
+ def getDbTypeInfo
540
+ return @typeInfo if @typeInfo
541
+
542
+ begin
543
+ stmt = @connection.types
544
+ @typeInfo = stmt.fetch_all
545
+ rescue Exception => e
546
+ raise ActiveRecordError, e.message
547
+ ensure
548
+ stmt.drop unless stmt.nil?
549
+ end
550
+ @typeInfo
551
+ end
552
+
553
+ def dbmsIdentCase(identifier)
554
+ # Assume received identifier is in ActiveRecord case.
555
+ case @connection.get_info(ODBC::SQL_IDENTIFIER_CASE)
556
+ when ODBC::SQL_IC_UPPER
557
+ identifier =~ /[A-Z]/ ? identifier : identifier.upcase
558
+ else
559
+ identifier
560
+ end
561
+ end
562
+ # Converts an identifier to the case conventions used by ActiveRecord.
563
+ def activeRecIdentCase(identifier)
564
+ # Assume received identifier is in DBMS's data dictionary case.
565
+ case @connection.get_info(ODBC::SQL_IDENTIFIER_CASE)
566
+ when ODBC::SQL_IC_UPPER
567
+ identifier =~ /[a-z]/ ? identifier : identifier.downcase
568
+ else
569
+ identifier
570
+ end
571
+ end
572
+
573
+ # Converts a result set value from an ODBC type to an ActiveRecord
574
+ # generic type.
575
+ def convertOdbcValToGenericVal(value)
576
+ # When fetching a result set, the Ruby ODBC driver converts all ODBC
577
+ # SQL types to an equivalent Ruby type; with the exception of
578
+ # SQL_TYPE_DATE, SQL_TYPE_TIME and SQL_TYPE_TIMESTAMP.
579
+ #
580
+ # The conversions below are consistent with the mappings in
581
+ # ODBCColumn#mapOdbcSqlTypeToGenericType and Column#klass.
582
+ res = value
583
+ case value
584
+ when ODBC::TimeStamp
585
+ res = Time.gm(value.year, value.month, value.day, value.hour,
586
+ value.minute, value.second)
587
+ when ODBC::Time
588
+ now = DateTime.now
589
+ res = Time.gm(now.year, now.month, now.day, value.hour,
590
+ value.minute, value.second)
591
+ when ODBC::Date
592
+ res = Date.new(value.year, value.month, value.day)
593
+ end
594
+ res
595
+ end
596
+ end
597
+
598
+ class MimerColumn < Column #:nodoc:
599
+
600
+ def initialize (name, tableName, default, odbcSqlType, nativeType,
601
+ null = true, limit = nil, typeInfo = nil)
602
+ @name, @null = name, null
603
+
604
+ # Only set @limit if native type takes a creation parameter
605
+ nativeTypeTakesCreateParams = false
606
+ if typeInfo
607
+ typeInfo.each do |row|
608
+ dbmsType = row[0] #SQLGetTypeInfo: TYPE_NAME
609
+ if dbmsType.casecmp(nativeType) == 0
610
+ createParams = row[5]
611
+ nativeTypeTakesCreateParams = (createParams && createParams.strip.length > 0)
612
+ break;
613
+ end
614
+ end
615
+ end
616
+ @limit = nativeTypeTakesCreateParams ? limit : nil
617
+
618
+ # nativeType is DBMS type used for column definition
619
+ # sql_type assigned here excludes any length specification
620
+ @sql_type = @nativeType = String.new(nativeType)
621
+ @type = mapOdbcSqlTypeToGenericType(odbcSqlType)
622
+ # type_cast uses #type so @type must be set first
623
+
624
+ # The MS SQL Native Client ODBC driver wraps defaults in parentheses
625
+ # (contrary to the ODBC spec).
626
+ # e.g. '(1)' instead of '1', '(null)' instead of 'null'
627
+ if default =~ /^\((.+)\)$/ then default = $1 end
628
+
629
+ if self.respond_to?(:default_preprocess, true)
630
+ default_preprocess(nativeType, default)
631
+ end
632
+
633
+ @default = type_cast(default)
634
+ @table = tableName
635
+ @text = [:string, :text].include? @type
636
+ @number = [:float, :integer].include? @type
637
+ @primary = nil
638
+ @autounique = self.respond_to?(:autoUnique?, true) ? autoUnique? : false
639
+ end
640
+
641
+ # Casts a value to the Ruby class corresponding to the ActiveRecord
642
+ # abstract type associated with the column.
643
+ #
644
+ # See Column#klass for the Ruby class corresponding to each
645
+ # ActiveRecord abstract type.
646
+ #
647
+ # This method is not just called by the MimerColumn constructor, so
648
+ # value may be something other than a String.
649
+ #
650
+ # When casting a column's default value:
651
+ # nil => no default value specified
652
+ # "'<value>'" => string default value
653
+ # "NULL" => default value of NULL
654
+ # "TRUNCATED" => default value can't be represented without truncation
655
+ def type_cast(value)
656
+ return nil if value.nil? || value =~
657
+ /(^\s*[(]*\s*null\s*[)]*\s*$)|(^\s*truncated\s*$)/i
658
+ case type
659
+ when :string then value.to_s
660
+ when :text then value.to_s
661
+ when :integer then value.to_i
662
+ when :float then value.to_f
663
+ when :boolean then self.class.value_to_boolean(value)
664
+ when :binary then value.to_s
665
+ when :datetime then self.class.value_to_time(value)
666
+ when :timestamp then self.class.value_to_time(value)
667
+ when :time then self.class.value_to_time(value)
668
+ when :date then self.class.value_to_date(value)
669
+ else
670
+ raise ActiveRecordError, "Unknown ActiveRecord abstract type"
671
+ end
672
+ end
673
+
674
+ def self.string_to_binary(value)
675
+ return "X'#{value.unpack("C*").collect {|v| v.to_s(16)}.join}'"
676
+ end
677
+
678
+ def self.value_to_time(value)
679
+ # If we received a time literal without a date component, pad the
680
+ # resulting array with dummy date information.
681
+ #
682
+ # See Column#string_to_dummy_time and
683
+ # BasicsTest#test_attributes_on_dummy_time. Both assume the dummy
684
+ # date component will be 2000-01-01.
685
+ if value.is_a?(Time)
686
+ if value.year != 0 and value.month != 0 and value.day != 0
687
+ return value
688
+ else
689
+ return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil
690
+ end
691
+ elsif value.is_a?(String)
692
+ time = ParseDate.parsedate(value)
693
+ if time[0].nil? && time[1].nil? && time[2].nil?
694
+ time[0] = 2000; time[1] = 1; time[2] = 1;
695
+ end
696
+ Time.send(Base.default_timezone, *time) rescue nil
697
+ else
698
+ raise ActiveRecordError, "Unexpected type (#{value.class})"
699
+ end
700
+ end
701
+
702
+ def self.value_to_date(value)
703
+ if value.is_a?(Date)
704
+ return value
705
+ elsif value.is_a?(String)
706
+ begin
707
+ date = ParseDate.parsedate(value)
708
+ Date.new(date[0], date[1], date[2])
709
+ rescue
710
+ raise ActiveRecordError, "Cannot convert supplied String value to Date (#{value})"
711
+ end
712
+ elsif value.is_a?(Time)
713
+ begin
714
+ Date.new(value.year, value.month, value.day)
715
+ rescue
716
+ raise ActiveRecordError, "Cannot convert supplied Time value to Date (#{value})"
717
+ end
718
+ else
719
+ raise ActiveRecordError, "Unexpected type (#{value.class})"
720
+ end
721
+ end
722
+
723
+ private
724
+
725
+ # Maps an ODBC SQL type to an ActiveRecord abstract data type
726
+ #
727
+ # c.f. Mappings in ConnectionAdapters::Column#simplified_type based on
728
+ # native column type declaration
729
+ #
730
+ # See also:
731
+ # Column#klass (schema_definitions.rb) for the Ruby class corresponding
732
+ # to each abstract data type.
733
+ def mapOdbcSqlTypeToGenericType (odbcSqlType)
734
+ case odbcSqlType
735
+ when ODBC::SQL_BIT then :boolean
736
+
737
+ when ODBC::SQL_CHAR, ODBC::SQL_VARCHAR then :string
738
+ when ODBC::SQL_LONGVARCHAR then :text
739
+
740
+ when ODBC::SQL_WCHAR, ODBC::SQL_WVARCHAR then :string
741
+ when ODBC::SQL_WLONGVARCHAR then :text
742
+
743
+ when ODBC::SQL_TINYINT, ODBC::SQL_SMALLINT, ODBC::SQL_INTEGER,
744
+ ODBC::SQL_BIGINT then :integer
745
+
746
+ when ODBC::SQL_REAL, ODBC::SQL_FLOAT, ODBC::SQL_DOUBLE then :float
747
+
748
+ when ODBC::SQL_DECIMAL, ODBC::SQL_NUMERIC then :float
749
+ # SQL_DECIMAL & SQL_NUMERIC are both exact types.
750
+ # Possible loss of precision if retrieved to :float
751
+
752
+ when ODBC::SQL_BINARY, ODBC::SQL_VARBINARY,
753
+ ODBC::SQL_LONGVARBINARY then :binary
754
+
755
+ # SQL_DATETIME is an alias for SQL_DATE in ODBC's sql.h & sqlext.h
756
+ when ODBC::SQL_DATE, ODBC::SQL_TYPE_DATE,
757
+ ODBC::SQL_DATETIME then :date
758
+ when ODBC::SQL_TIME, ODBC::SQL_TYPE_TIME then :time
759
+ when ODBC::SQL_TIMESTAMP, ODBC::SQL_TYPE_TIMESTAMP then :timestamp
760
+
761
+ when ODBC::SQL_GUID then :string
762
+
763
+ else
764
+ # when SQL_UNKNOWN_TYPE
765
+ # (ruby-odbc driver doesn't support following ODBC SQL types:
766
+ # SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_INTERVAL_xxx)
767
+ msg = "Unsupported ODBC SQL type [" << odbcSqlType.to_s << "]"
768
+ raise ActiveRecordError, msg
769
+ end
770
+ end
771
+
772
+ end # class MimerColumn
773
+ end
774
+ end
775
+ rescue LoadError
776
+ module ActiveRecord # :nodoc:
777
+ class Base
778
+ def self.mimer_connection(config) # :nodoc:
779
+ raise LoadError, "The Ruby Mimer module could not be loaded."
780
+ end
781
+ end
782
+ end
783
+ end
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'active_record/connection_adapters/mimer_adapter'
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
4
+
5
+ require 'rubygems'
6
+ require 'active_record'
7
+ require 'mimer_adapter'
8
+
9
+ ActiveRecord::Base.establish_connection(:adapter=>'mimer',
10
+ :dsn => 'ar-mimer-testdb1',
11
+ :username=>'test_ar',
12
+ :password=>'test_ar')
13
+
14
+
15
+ ActiveRecord::Schema.define do
16
+ drop_table :authors rescue nil
17
+
18
+ create_table :authors, :force => true do |t|
19
+ t.column :name, :string, :null => false
20
+ end
21
+
22
+ # Exercise all types, and add_column
23
+ add_column :authors, :description, :text
24
+ add_column :authors, :descr, :string, :limit => 50
25
+ add_column :authors, :age, :integer, :null => false, :default => 17
26
+ add_column :authors, :weight, :float
27
+ add_column :authors, :born, :datetime
28
+ add_column :authors, :died, :timestamp
29
+ add_column :authors, :wakeup_time, :time
30
+ add_column :authors, :birth_date, :date
31
+ add_column :authors, :private_key, :binary
32
+ add_column :authors, :female, :boolean, :default => true
33
+
34
+ change_column :authors, :descr, :string, :limit => 100
35
+ change_column_default :authors, :female, false
36
+ remove_column :authors, :died
37
+
38
+ add_index :authors, :name, :unique
39
+ add_index :authors, [:age,:female], :name => :is_age_female
40
+
41
+ remove_index :authors, :name
42
+ remove_index :authors, :name => :is_age_female
43
+
44
+ create_table :products, :force => true do |t|
45
+ t.column :title, :string
46
+ t.column :description, :text
47
+ t.column :image_url, :string
48
+ end
49
+
50
+ add_column :products, :price, :float, :default => 0.0
51
+
52
+ create_table :orders, :force => true do |t|
53
+ t.column :name, :string
54
+ t.column :address, :text
55
+ t.column :email, :string
56
+ t.column :pay_type, :string, :limit => 10
57
+ end
58
+ create_table :line_items, :force => true do |t|
59
+ t.column :product_id, :integer, :null => false
60
+ t.column :order_id, :integer, :null => false
61
+ t.column :quantity, :integer, :null => false
62
+ t.column :total_price, :float, :null => false
63
+ end
64
+ end
65
+
66
+ class Author < ActiveRecord::Base; end
67
+
68
+ class Order < ActiveRecord::Base
69
+ has_many :line_items
70
+ end
71
+
72
+ class Product < ActiveRecord::Base
73
+ has_many :orders, :through => :line_items
74
+ has_many :line_items
75
+
76
+ def self.find_products_for_sale
77
+ find(:all, :order => "title")
78
+ end
79
+ end
80
+
81
+ class LineItem < ActiveRecord::Base
82
+ belongs_to :order
83
+ belongs_to :product
84
+ end
85
+
86
+ Product.create(:title => 'Pragmatic Project Automation',
87
+ :description =>
88
+ %{<p>
89
+ <em>Pragmatic Project Automation</em> shows you how to improve the
90
+ consistency and repeatability of your projects procedures using
91
+ automation to reduce risk and errors.
92
+ </p>
93
+ <p>
94
+ Simply put, were going to put this thing called a computer to work
95
+ for you doing the mundane (but important) project stuff. That means
96
+ youll have more time and energy to do the really
97
+ exciting---and difficult---stuff, like writing quality code.
98
+ </p>},
99
+ :image_url => '/images/auto.jpg',
100
+ :price => 29.95)
101
+
102
+
103
+ Product.create(:title => 'Pragmatic Version Control',
104
+ :description =>
105
+ %{<p>
106
+ This book is a recipe-based approach to using Subversion that will
107
+ get you up and
108
+ running quickly---and correctly. All projects need version control:
109
+ its a foundational piece of any projects infrastructure. Yet half
110
+ of all project teams in the U.S. dont use any version control at all.
111
+ Many others dont use it well, and end up experiencing time-consuming problems.
112
+ </p>},
113
+ :image_url => '/images/svn.jpg',
114
+ :price => 28.50)
115
+
116
+ Product.create(:title => 'Pragmatic Unit Testing (C#)',
117
+ :description =>
118
+ %{<p>
119
+ Pragmatic programmers use feedback to drive their development and
120
+ personal processes. The most valuable feedback you can get while
121
+ coding comes from unit testing.
122
+ </p>
123
+ <p>
124
+ Without good tests in place, coding can become a frustrating game of
125
+ "whack-a-mole." Thats the carnival game where the player strikes at a
126
+ mechanical mole; it retreats and another mole pops up on the opposite side
127
+ of the field. The moles pop up and down so fast that you end up flailing
128
+ your mallet helplessly as the moles continue to pop up where you least
129
+ expect them.
130
+ </p>},
131
+ :image_url => '/images/utc.jpg',
132
+ :price => 27.75)
133
+
134
+ Author.destroy_all
135
+ Author.create(:name => "Arne Svensson", :age => 30)
136
+ Author.create(:name => "Pelle Gogolsson", :age => 15, :wakeup_time => Time.now, :private_key => "afbafddsfgsdfg")
137
+ Author.find(:first)
138
+ Author.find(:all)
139
+ arne = Author.find(:first)
140
+ arne.destroy
141
+ pelle = Author.find(:first)
142
+ pelle.name = "Pelle Sweitchon"
143
+ pelle.description = "dfsssdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
144
+ pelle.descr = "adsfasdf"
145
+ pelle.age = 79
146
+ pelle.weight = 57.6
147
+ pelle.born = Time.gm(1982,8,13,10,15,3,0)
148
+ pelle.female = false
149
+ pelle.save
150
+
151
+ prods = Product.find_all
152
+ order = Order.new(:name => "Dalai Lama", :address => "Great Road 32", :email => "abc@dot.com", :pay_type => "cash")
153
+ order.line_items << LineItem.new(:product => prods[0], :quantity => 3, :total_price => (prods[0].price * 3))
154
+ order.line_items << LineItem.new(:product => prods[2], :quantity => 1, :total_price => (prods[2].price))
155
+ order.save
156
+ order.line_items.first.order.line_items.first.product.orders.first
157
+
158
+ #puts "order: #{order.line_items.inspect}, with id: #{order.id} and name: #{order.name}"
159
+
160
+ ActiveRecord::Schema.define do
161
+ drop_table :line_items
162
+ drop_table :orders
163
+ drop_table :products
164
+ drop_table :authors
165
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: activerecord-mimer
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2006-11-08 00:00:00 +01:00
8
+ summary: Mimer support for ActiveRecord.
9
+ require_paths:
10
+ - lib
11
+ email: ola@ologix.com
12
+ homepage: http://ar-mimer.rubyforge.org/
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: mimer_adapter
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Ola Bini
31
+ files:
32
+ - lib/active_record
33
+ - lib/mimer_adapter.rb
34
+ - lib/active_record/connection_adapters
35
+ - lib/active_record/connection_adapters/mimer_adapter.rb
36
+ - test/basic_test.rb
37
+ - LICENSE
38
+ - README
39
+ test_files: []
40
+
41
+ rdoc_options: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ executables: []
46
+
47
+ extensions: []
48
+
49
+ requirements: []
50
+
51
+ dependencies: []
52
+