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 +21 -0
- data/README +70 -0
- data/lib/active_record/connection_adapters/mimer_adapter.rb +783 -0
- data/lib/mimer_adapter.rb +3 -0
- data/test/basic_test.rb +165 -0
- metadata +52 -0
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
|
data/test/basic_test.rb
ADDED
@@ -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
|
+
|