activerecord-mimer 0.0.1

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