yob-activemdb 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt ADDED
@@ -0,0 +1,69 @@
1
+ ActiveMDB
2
+ by Matthew King
3
+ http://rubyforge.org/projects/activemdb/
4
+
5
+ == DESCRIPTION:
6
+
7
+ ActiveMDB is a developer's tool for exploration and migration of MS Access (.mdb) files. It uses ActiveRecord-ish reflection to parse table and column names and provide attribute readers. Yes, it is *READ ONLY*. ActiveMDB is little more than a wrapper of varying thickness around the utilities from Brian Bruns's MDB Tools project (http://mdbtools.sourceforge.net/). Kudos to Mr. Bruns.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * MDBTools - Straightforward wrapper around the CLI tools that you get with libmdb
12
+ * ActiveMDB::Base - Subclass to make your models, just like the big shots do.
13
+
14
+
15
+ == SYNOPSIS:
16
+
17
+ # When your Access database schema conforms to the 37s stylebook:
18
+ class Bacon < ActiveMDB::Base
19
+ set_mdb_file 'db/wherefore.mdb'
20
+ end
21
+
22
+ # in the find_* methods, the entries in the hash
23
+ # get turned into "WHERE #{key} like %#{value}%" conditions,
24
+ # which fails when the column is a boolean, which is a regression from 0.1.0.
25
+ # I could fix this tonight, but my son is yelling at me to come out for dinner.
26
+ best_bacon = Bacon.find_all(:rind => 'creamy', :sodium_content => 'Awesome!' )
27
+
28
+ # When it doesn't:
29
+ class Employee < ActiveMDB::Base
30
+ set_mdb_file 'db/sample.mdb'
31
+ set_table_name 'Employee'
32
+ set_primary_key 'Emp_Id'
33
+ end
34
+
35
+ paula = Employee.find_first(:Department => 'Engineering', :Specialty => 'paulaBeans')
36
+
37
+
38
+ == REQUIREMENTS:
39
+
40
+ * http://mdbtools.sourceforge.net/
41
+
42
+ == INSTALL:
43
+
44
+ * Sadly, no easy install of MDB Tools at this time. It compiles on Mac OS X 10.4.x, both PPC and Intel. Haven't tested on Linuxes yet, but that's what Parallels is for, right?
45
+
46
+ == LICENSE:
47
+
48
+ (The MIT License)
49
+
50
+ Copyright (c) 2007
51
+
52
+ Permission is hereby granted, free of charge, to any person obtaining
53
+ a copy of this software and associated documentation files (the
54
+ 'Software'), to deal in the Software without restriction, including
55
+ without limitation the rights to use, copy, modify, merge, publish,
56
+ distribute, sublicense, and/or sell copies of the Software, and to
57
+ permit persons to whom the Software is furnished to do so, subject to
58
+ the following conditions:
59
+
60
+ The above copyright notice and this permission notice shall be
61
+ included in all copies or substantial portions of the Software.
62
+
63
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
64
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
65
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
66
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
67
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
68
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
69
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/active_mdb.rb'
6
+
7
+ Hoe.new('activemdb', ActiveMDB::VERSION) do |p|
8
+ p.rubyforge_name = 'activemdb'
9
+ p.author = 'Matthew King'
10
+ p.email = 'automatthew@gmail.com'
11
+ p.summary = 'ActiveRecordy wrapper around MDB Tools, allowing POSIX platforms to read MS Access (.mdb) files'
12
+ p.description = p.paragraphs_of('README.txt', 2).join("\n\n")
13
+ p.url = 'http://activemdb.rubyforge.org/'
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ end
16
+
17
+ # vim: syntax=Ruby
data/lib/active_mdb.rb ADDED
@@ -0,0 +1,19 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+ module ActiveMDB
3
+ VERSION = '0.2.2'
4
+ end
5
+
6
+
7
+ require 'rubygems'
8
+ require 'active_support'
9
+ require 'active_mdb/mdb_tools'
10
+ load 'active_mdb/mdb.rb'
11
+ load 'active_mdb/table.rb'
12
+ load 'active_mdb/record.rb'
13
+ load 'active_mdb/column.rb'
14
+ load 'active_mdb/base.rb'
15
+ require 'faster_csv'
16
+
17
+
18
+
19
+
@@ -0,0 +1,184 @@
1
+ module ActiveMDB
2
+ # Hi, I am an imitation of ActiveRecord.
3
+ class Base
4
+
5
+ def initialize(attributes=nil)
6
+ @attributes = attributes unless attributes.nil?
7
+ end
8
+
9
+ cattr_accessor :pluralize_table_names, :instance_writer => false
10
+ @@pluralize_table_names = true
11
+
12
+ class << self # Class methods
13
+ attr_accessor :mdb_file
14
+ attr_reader :mdb
15
+
16
+ # set the path to the MDB file
17
+ def set_mdb_file(file)
18
+ @mdb_file = file
19
+ @mdb = MDB.new(file)
20
+ end
21
+
22
+ # set the name of the table in the MDB file
23
+ def table_name
24
+ table_name = Inflector.underscore(Inflector.demodulize(self.to_s))
25
+ table_name = Inflector.pluralize(table_name) if pluralize_table_names
26
+ table_name
27
+ end
28
+
29
+ # borrowed from ActiveRecord
30
+ def set_table_name(value = nil, &block)
31
+ define_attr_method :table_name, value, &block
32
+ end
33
+ alias :table_name= :set_table_name
34
+
35
+ # Returns an array of column objects for the table associated with this class.
36
+ def columns
37
+ unless @columns
38
+ @columns = @mdb.columns(table_name)
39
+ # @columns.each {|column| column.primary = column.name == primary_key}
40
+ end
41
+ @columns
42
+ end
43
+
44
+ def column_names
45
+ @mdb.column_names(table_name)
46
+ end
47
+
48
+ # returns 'id' unless you overwrite this method using set_primary_key
49
+ def primary_key
50
+ 'id'
51
+ end
52
+
53
+ # specify the field to be used as a primary key for those
54
+ # operations that require one. At this time, that isn't really
55
+ # anything except count.
56
+ def set_primary_key(value = nil, &block)
57
+ define_attr_method :primary_key, value, &block
58
+ end
59
+ alias :primary_key= :set_primary_key
60
+
61
+ def table_exists?
62
+ @mdb.tables.include? table_name
63
+ end
64
+
65
+ # How many? Requires that the primary_key return a valid field.
66
+ def count
67
+ @mdb.count(table_name, primary_key)
68
+ end
69
+
70
+ # the conditions hash keys are column names, the values are search values
71
+ # find_first :superhero_name => 'The Ironist', :powers => 'Wit'
72
+ #
73
+ # mdb-sql doesn't implement LIMIT yet, so this method pulls all results and
74
+ # calls Array#first on them. Ooky.
75
+ def find_first(conditions_hash)
76
+ # rekey_hash(conditions_hash)
77
+ find_from_hash(conditions_hash).first
78
+ end
79
+
80
+ # the conditions hash keys are column names, the values are search values
81
+ # find_all :superhero_name => 'The Ironist', :powers => 'Wit'
82
+ def find_all(conditions_hash = {})
83
+ find_from_hash(conditions_hash)
84
+ end
85
+
86
+ # borrowed from ActiveRecord.
87
+ def define_attr_method(name, value=nil, &block)
88
+ sing = class << self; self; end
89
+ sing.send :alias_method, "original_#{name}", name
90
+ if block_given?
91
+ sing.send :define_method, name, &block
92
+ else
93
+ # use eval instead of a block to work around a memory leak in dev
94
+ # mode in fcgi
95
+ sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
96
+ end
97
+ end
98
+
99
+ # relies on stuff that doesn't work right now
100
+ def rekey_hash(conditions_hash)
101
+ conditions_hash.each do |key,value|
102
+ column = self[key.to_s]
103
+ if column.boolean?
104
+ key = column.name + '!'
105
+ else
106
+ key = column.name
107
+ end
108
+ end
109
+ end
110
+
111
+ # supply a conditions string that would nestle gently in a
112
+ # WHERE clause, after the WHERE but before the.
113
+ def find_where(conditions)
114
+ MDBTools.sql_select_where(mdb_file, table_name, nil, conditions).collect! { |record| instantiate(record) }
115
+ end
116
+
117
+ # takes a hash where the keys are column names (string or symbol)
118
+ # and the values are the associated values.
119
+ def find_from_hash(hash)
120
+ conditions = conditions_from_hash(hash)
121
+ find_where(conditions)
122
+ end
123
+
124
+ # the conditions hash keys are column names, the values are search values
125
+ # e.g. search_with_hash(:first_name => 'Matthew', :last_name => 'King')
126
+ def conditions_from_hash(hash)
127
+ return nil if hash.nil? || hash.empty?
128
+ MDBTools.compile_conditions(hash) do |column_name, value|
129
+ column = column_for_method(column_name) || column_for_field(column_name)
130
+ raise ArgumentError, "No column corresponding to #{column_name}" unless column
131
+ case column.klass.to_s
132
+ when 'Fixnum', 'Float'
133
+ "#{column.name} = #{value}"
134
+ when 'String'
135
+ "#{column.name} LIKE '%#{value}%'"
136
+ when 'Object'
137
+ value = value ? 1 : 0
138
+ "#{column.name} IS #{value}"
139
+ end
140
+ end
141
+ end
142
+
143
+ # given the name of a column, return the Column object
144
+ def column_for_field(column_name)
145
+ columns.detect {|c| c.name == column_name.to_s}
146
+ end
147
+
148
+ def column_for_method(method_name)
149
+ columns.detect {|c| c.method_name == method_name.to_s}
150
+ end
151
+
152
+ private
153
+
154
+ def instantiate(record)
155
+ new_hash = {}
156
+ record.each do |name,value|
157
+ # begin
158
+ new_hash[MDBTools.methodize(name)] = column_for_field(name).type_cast(value)
159
+ # rescue
160
+ # raise "No column for #{name}"
161
+ # end
162
+ end
163
+ self.new new_hash
164
+ end
165
+
166
+
167
+ end
168
+
169
+ private
170
+
171
+ def method_missing(method_id, *args, &block)
172
+ method_name = MDBTools.methodize(method_id.to_s)
173
+ spaced_method_name = method_name.gsub("_"," ")
174
+ if @attributes.include?(method_name)
175
+ value = @attributes[method_name]
176
+ elsif @attributes.include?(spaced_method_name)
177
+ value = @attributes[spaced_method_name]
178
+ else
179
+ super
180
+ end
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,69 @@
1
+ class Column
2
+ include MDBTools
3
+
4
+ attr_reader :method_name, :name, :type, :size
5
+
6
+ def initialize(name, type, size)
7
+ @name, @type = name, type
8
+ @method_name, @size = methodize(name), size.to_i
9
+ end
10
+
11
+ # returns a new Column from a hash with the keys:
12
+ # 'Column Name', 'Type', and 'Size'
13
+ def self.new_from_describe(describe_hash)
14
+ self.new(describe_hash["Column Name"], describe_hash["Type"], describe_hash["Size"])
15
+ end
16
+
17
+ # return Ruby class corresponding to data type.
18
+ # Borrowed from ActiveRecord
19
+ def klass
20
+ case type
21
+ when 'Text', 'Character' then String
22
+ when 'Long Integer' then Fixnum
23
+ when 'Double' then Float
24
+ when 'Currency', 'Float' then Float
25
+ when 'DateTime (Short)' then Time
26
+ when 'Boolean' then Object
27
+ when 'Decimal' then BigDecimal
28
+ when 'Binary' then String
29
+ end
30
+ end
31
+
32
+ # Casts value (which is a String) to an appropriate instance.
33
+ # Totally borrowed from ActiveRecord
34
+ def type_cast(value)
35
+ return nil if value.nil?
36
+ case type
37
+ when 'Text', 'Character' then value
38
+ when 'Long Integer' then value.to_i rescue value ? 1 : 0
39
+ when 'Currency', 'Float' then value.to_f
40
+ when 'Double' then value.to_f
41
+ when 'DateTime (Short)' then self.class.string_to_time(value)
42
+ when 'Boolean' then self.class.value_to_boolean(value)
43
+ when 'Decimal' then self.class.value_to_decimal(value)
44
+ when 'Binary' then self.class.binary_to_string(value)
45
+ else value
46
+ end
47
+ end
48
+
49
+ def self.string_to_time(string)
50
+ string
51
+ end
52
+
53
+ # provided any argument, returns the mdb-tools version
54
+ # of truth (which is to say, 1 or 0)
55
+ def self.value_to_boolean(value)
56
+ if value == true || value == false
57
+ value
58
+ else
59
+ %w(true t 1).include?(value.to_s.downcase)
60
+ end
61
+ end
62
+
63
+ # Are you a Boolean?
64
+ def boolean?
65
+ self.type == 'Boolean'
66
+ end
67
+
68
+
69
+ end
@@ -0,0 +1,50 @@
1
+ # require 'mdb_tools'
2
+
3
+ class MDB
4
+ include MDBTools
5
+
6
+ attr_reader :mdb_file, :prefix, :exclude, :tables
7
+
8
+ def initialize(mdb_file, options = {})
9
+ @mdb_file = check_file(mdb_file)
10
+ @prefix = options[:prefix] || ''
11
+ @exclude, @include = options[:exclude], options[:include]
12
+ @export_syntax = options[:sql_syntax] || 'mysql'
13
+ @tables = mdb_tables(@mdb_file, :exclude => @exclude, :include => @include)
14
+ # @tables = create_table_objects
15
+ end
16
+
17
+ # consumer of poor, weak MDBTools.faked_count
18
+ def count(table_name, attribute)
19
+ MDBTools.faked_count(@mdb_file, table_name, attribute)
20
+ end
21
+
22
+ def columns(table_name)
23
+ MDBTools.describe_table(@mdb_file, table_name).map do |column|
24
+ Column.new_from_describe(column)
25
+ end
26
+ end
27
+
28
+ def column_names(table_name)
29
+ MDBTools.field_names_for(@mdb_file, table_name)
30
+ end
31
+
32
+
33
+ private
34
+
35
+ # Deprecated.
36
+ def create_table_objects
37
+ tables = {}
38
+ @table_names.each do |table|
39
+ tables[table] = Table.new(self, table, @prefix)
40
+ metaclass = class << self; self; end
41
+ metaclass.send :define_method, methodize(table) do
42
+ tables[table]
43
+ end
44
+ end
45
+ tables
46
+ end
47
+
48
+
49
+ end
50
+
@@ -0,0 +1,229 @@
1
+ module MDBTools
2
+
3
+ extend self
4
+
5
+ DELIMITER = '::'
6
+ LINEBREAK = "\n"
7
+ SANITIZER = /^\w\.\_/ # dumb filter for SQL arguments
8
+ BACKENDS = %w{ access mysql oracle postgres sybase }
9
+
10
+ # test for existence and usability of file
11
+ def check_file(mdb_file)
12
+ raise ArgumentError, "File not found: #{mdb_file}" unless File.exist?(mdb_file)
13
+ @mdb_version = `mdb-ver #{mdb_file} 2>&1`.chomp
14
+ if $? != 0
15
+ raise ArgumentError, "mdbtools cannot access #{mdb_file}"
16
+ end
17
+ mdb_file
18
+ end
19
+
20
+ # runs mdb_version. A blank version indicates an unusable file
21
+ def valid_file?(file)
22
+ !mdb_version(file).blank?
23
+ end
24
+
25
+ def mdb_version(file)
26
+ `mdb-ver #{file} 2> /dev/null`.chomp
27
+ end
28
+
29
+ # raises an ArgumentError unless the mdb file contains a table with the specified name.
30
+ # returns the table name, otherwise.
31
+ def check_table(mdb_file, table_name)
32
+ unless mdb_tables(mdb_file).include?(table_name)
33
+ raise ArgumentError, "mdbtools does not think a table named \"#{table_name}\" exists"
34
+ end
35
+ table_name
36
+ end
37
+
38
+ # uses mdb-tables tool to return an array of table names.
39
+ # You can filter the tables by passing an array of strings as
40
+ # either the :exclude or :include key to the options hash.
41
+ # The strings will be ORed into a regex. Only one or the other of
42
+ # :exclude or :include, please.
43
+ #
44
+ # ex. mdb_tables('thing.mdb', :exclude => ['_Lookup'])
45
+ #
46
+ # ex. mdb_tables('thing.mdb', :include => ['tbl'])
47
+ def mdb_tables(mdb_file, options = {})
48
+ included, excluded = options[:include], options[:exclude]
49
+ return `mdb-tables -1 #{mdb_file}`.split(LINEBREAK) if not (included || excluded)
50
+ raise ArgumentError if (options[:include] && options [:exclude])
51
+ if options[:exclude]
52
+ regex = Regexp.new options[:exclude].to_a.join('|')
53
+ tables = `mdb-tables -1 #{mdb_file}`.split(LINEBREAK).delete_if { |name| name =~ regex }
54
+ end
55
+ if options[:include]
56
+ regex = Regexp.new options[:include].to_a.join('|')
57
+ tables = `mdb-tables -1 #{mdb_file}`.split(LINEBREAK).select { |name| name =~ regex }
58
+ end
59
+ tables
60
+ end
61
+
62
+ # takes an array of field names
63
+ # and some conditions to append in a WHERE clause
64
+ def sql_select_where(mdb_file, table_name, attributes = nil, conditions=nil)
65
+ if attributes.respond_to?(:join)
66
+ attributes = attributes.collect {|a| "\"#{a}\"" }.join(' ')
67
+ elsif attributes.kind_of?(String)
68
+ attributes = "\"#{attributes}\""
69
+ else
70
+ attributes ||= '*'
71
+ end
72
+ where = conditions ? "where #{conditions}" : ""
73
+ sql = "select #{attributes} from #{table_name} #{where}"
74
+ mdb_sql(mdb_file, sql)
75
+ end
76
+
77
+ # forks an IO.popen running mdb-sql and discarding STDERR to /dev/null.
78
+ # The sql argument should be a single statement, 'cause I don't know
79
+ # what will happen otherwise. mdb-sql uses "\ngo" as the command terminator.
80
+ def mdb_sql(mdb_file,sql)
81
+ # libMDB barks on stderr quite frequently, so discard stderr entirely
82
+ command = "mdb-sql -Fp -d '#{DELIMITER}' #{mdb_file} 2> /dev/null \n"
83
+ array = []
84
+ IO.popen(command, 'r+') do |pipe|
85
+ pipe << "#{sql}\ngo\n"
86
+ pipe.close_write
87
+ pipe.readline
88
+ fields = pipe.readline.chomp.split(DELIMITER)
89
+ pipe.each do |row|
90
+ hash = {}
91
+ row = row.chomp.split(DELIMITER)
92
+ fields.each_index do |i|
93
+ hash[fields[i]] = row[i]
94
+ end
95
+ array << hash
96
+ end
97
+ end
98
+ array
99
+ end
100
+
101
+ # uses mdb-sql to retrieve an array of the table's field names
102
+ def field_names_for(mdb_file, table)
103
+ fields = `echo -n 'select * from #{table} where 1 = 2' | mdb-sql -Fp -d '#{DELIMITER}' #{mdb_file}`.chomp.sub(/^\n+/, '')
104
+ fields.split(DELIMITER)
105
+ end
106
+
107
+
108
+ # takes a hash where keys are column names, values are search values
109
+ # and returns a string that you can use in a WHERE clause
110
+ #
111
+ # ex. compile_conditions(:first_name => 'Summer', :last_name => 'Roberts')
112
+ # gives "first_name like '%Summer%' AND last_name like '%Roberts%'
113
+ #
114
+ # if you want to use an operator other than LIKE, give compile_conditions
115
+ # a block that accepts column_name and value and does something interesting
116
+ #
117
+ # compile_conditions(:age => 18) {|name, value| "#{name} = #{value}"}
118
+ #
119
+ # the condition phrases are all ANDed together before insertion into a WHERE clause
120
+ def compile_conditions(conditions_hash)
121
+ conditions = conditions_hash.sort_by{|k,v| k.to_s}.map do |column_name, value|
122
+ if block_given?
123
+ yield column_name, value
124
+ else
125
+ "#{column_name} like '%#{value}%'"
126
+ end
127
+ end.join(' AND ')
128
+ end
129
+
130
+ # really dumb way to get a count. Does a SELECT and call size on the results
131
+ def faked_count(*args)
132
+ sql_select_where(*args).size
133
+ end
134
+
135
+ # convenience method, not really used with ActiveMDB.
136
+ # Valid options are :format, :headers, and :sanitize,
137
+ # which correspond rather directly to the underlying mdb-export arguments.
138
+ # Defaults to :format => 'sql', :headers => false, :sanitize => true
139
+ def mdb_export(mdb_file, table_name, options = {})
140
+ defaults = { :format => 'sql',
141
+ :headers => false,
142
+ :sanitize => true }
143
+ options = defaults.merge options
144
+
145
+ args = []
146
+ if options[:delimiter]
147
+ args << "-d #{options[:delimiter].dump}"
148
+ elsif options[:format] == 'sql'
149
+ args << "-I "
150
+ elsif options[:format] == 'csv'
151
+ args << "-d ',' "
152
+ else
153
+ raise ArgumentError, "Unknown format: #{options[:format]}"
154
+ end
155
+
156
+ args << "-H " unless options[:headers] == true
157
+ args << "-S" unless options[:sanitize] == false
158
+ `mdb-export #{args} #{mdb_file} #{table_name.to_s.dump}`
159
+ end
160
+
161
+ # wrapper for DESCRIBE TABLE using mdb-sql
162
+ def describe_table(mdb_file, table_name)
163
+ command = "describe table \"#{table_name}\""
164
+ mdb_sql(mdb_file,command)
165
+ end
166
+
167
+ # wrapper for mdb-schema, returns SQL statements
168
+ def mdb_schema(mdb_file, table_name)
169
+ schema = `mdb-schema -T #{table_name.dump} #{mdb_file}`
170
+ end
171
+
172
+ # convenience method for mdb_export to output CSV with headers.
173
+ def table_to_csv(mdb_file, table_name)
174
+ mdb_export(mdb_file, table_name, :format => 'csv', :headers => true)
175
+ end
176
+
177
+ def delimited_to_arrays(text)
178
+ text.gsub!(/\r\n/,' ')
179
+ text.split(LINEBREAK).collect { |row| row.split(DELIMITER)}
180
+ end
181
+
182
+ def arrays_to_hashes(headers, arrays)
183
+ arrays.collect do |record|
184
+ record_hash = Hash.new
185
+ until record.empty? do
186
+ headers.each do |header|
187
+ record_hash[header] = record.shift
188
+ end
189
+ end
190
+ record_hash
191
+ end
192
+ end
193
+
194
+
195
+ # helper to turn table names into standard format method names.
196
+ # Inside, it's just ActionView::Inflector.underscore
197
+ def methodize(table_name)
198
+ Inflector.underscore table_name
199
+ end
200
+
201
+ def backends
202
+ BACKENDS
203
+ end
204
+
205
+ # poor, weakly sanitizing gsub!.
206
+ def sanitize!(string)
207
+ string.gsub!(SANITIZER, '')
208
+ end
209
+
210
+ # mdb-tools recognizes 1 and 0 as the boolean values.
211
+ # Make it so.
212
+ def mdb_truth(value)
213
+ case value
214
+ when false
215
+ 0
216
+ when true
217
+ 1
218
+ when 0
219
+ 0
220
+ when 1
221
+ 1
222
+ when "0"
223
+ 0
224
+ when "1"
225
+ 1
226
+ end
227
+ end
228
+
229
+ end
@@ -0,0 +1,34 @@
1
+ # require 'mdb_tools'
2
+
3
+ # Deprecated way more than Table
4
+ class Record
5
+ include MDBTools
6
+ include Enumerable
7
+
8
+ def initialize(mdb_table, line)
9
+ raise 'no results' unless line
10
+ @struct = mdb_table.record_struct
11
+ @data = @struct.new(*line)
12
+ create_accessors
13
+ end
14
+
15
+ def each(&block)
16
+ @data.members.each {|k| yield k, @data[k] }
17
+ end
18
+
19
+ def compact
20
+ self.select {|k,v| v && !v.empty? && v != "0" }
21
+ end
22
+
23
+ private
24
+
25
+ def create_accessors
26
+ meta = class << self; self; end
27
+ @struct.members.each do |att|
28
+ meta.send :define_method, att do
29
+ @data[att]
30
+ end
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,95 @@
1
+ # require 'mdb_tools'
2
+
3
+ # Deprecated. So very deprecated.
4
+ class Table
5
+ include MDBTools
6
+
7
+ attr_reader :mdb_file, :table_name, :columns, :record_struct, :schema
8
+ attr_accessor :primary_key
9
+
10
+
11
+ def initialize(mdb, table_name, prefix)
12
+ @mdb_file = check_file(mdb.mdb_file)
13
+ @table_name = check_table(@mdb_file, table_name)
14
+ # @schema = mdb_schema(@mdb_file, @table_name)
15
+ @columns = describe_table(mdb_file, table_name).map do |column|
16
+ Column.new_from_describe(column)
17
+ end
18
+ @record_struct = create_record_struct
19
+ end
20
+
21
+
22
+ def [](method_name)
23
+ self.columns.detect {|c| c.method_name == method_name }
24
+ end
25
+
26
+ # returns an array of column names
27
+ def column_names
28
+ columns.collect {|x| methodize(x.method_name).to_sym}
29
+ end
30
+
31
+ def create_record_struct
32
+ attributes = columns.collect {|column| column.method_name.to_sym}
33
+ Struct.new( *attributes)
34
+ end
35
+
36
+ def to_csv
37
+ table_to_csv(mdb_file, table_name)
38
+ end
39
+
40
+ # returns the first record that meets the equality (sometimes LIKE) conditions
41
+ # of the supplied hash. No comparison operators available at the moment.
42
+ #
43
+ # find_first :superhero_name => 'The Ironist', :powers => 'Wit'
44
+ #
45
+ # mdb-sql doesn't implement LIMIT yet, so this method pulls all results and
46
+ # calls Array#first on them. Ooky.
47
+ def find_first(conditions_hash)
48
+ rekey_hash(conditions_hash)
49
+ result = sql_search(conditions_hash).first
50
+ create_record(result)
51
+ end
52
+
53
+
54
+ def find_all(conditions_hash={})
55
+ if conditions_hash.empty?
56
+ return sql_select_where(mdb_file, table_name, nil, '1 = 1').collect {|r| create_record(r)}
57
+ end
58
+ rekey_hash(conditions_hash)
59
+ sql_search(conditions_hash).collect {|r| create_record(r) }
60
+ end
61
+
62
+ private
63
+
64
+ def create_record(line)
65
+ Record.new(self, line)
66
+ end
67
+
68
+ def rekey_hash(conditions_hash)
69
+ conditions_hash.each do |key,value|
70
+ column = self[key.to_s]
71
+ if column.boolean?
72
+ key = column.name + '!'
73
+ else
74
+ key = column.name
75
+ end
76
+ end
77
+ end
78
+
79
+ # the conditions hash keys are column names, the values are search values
80
+ # e.g. sql_search(:first_name => 'Matthew', :last_name => 'King')
81
+ def sql_search(conditions_hash)
82
+ conditions = compile_conditions(conditions_hash) do |method_name,value|
83
+ column = self[method_name.to_s]
84
+ if column.boolean?
85
+ "#{column.name} = #{mdb_truth(value)}"
86
+ else
87
+ "#{column.name} like '%#{value}%'"
88
+ end
89
+ end
90
+ sql_select_where(mdb_file, table_name, nil, conditions)
91
+ end
92
+
93
+
94
+ end
95
+
@@ -0,0 +1,55 @@
1
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ require 'test/unit'
3
+ require File.join(File.dirname(__FILE__), 'test_helper')
4
+
5
+ class RealWorldTest < Test::Unit::TestCase
6
+
7
+ RETREAT = File.join(File.dirname(__FILE__), '..', 'db', 'retreat.mdb')
8
+
9
+ class Family < ActiveMDB::Base
10
+ set_mdb_file RETREAT
11
+ set_table_name 'tblFamilyData'
12
+ end
13
+
14
+ class Cabin < ActiveMDB::Base
15
+ set_mdb_file RETREAT
16
+ set_table_name 'tCabins'
17
+ end
18
+
19
+ def setup
20
+ @retreat = MDB.new(RETREAT)
21
+ end
22
+
23
+ def test_tables
24
+ tables = ["JobAssignmentsByCabin","Switchboard Items","tblAdultChildCode","tblCities","tblFamilyData","tblGender","tblIndividData","tblStatusChurch","tblStatusIndiv","tCabinAssignments","tCabins","tCampRates","tCampScholarships","tJobs","tJobsR","tParkAreas","tPayments","tRetreatEnrollment","tSponsors","tblStatusFamily"]
25
+ assert_equal tables, @retreat.tables
26
+ end
27
+
28
+ def test_column_names
29
+ column_names = ["FamKey","LName", "HisName", "HerName", "HmAddress", "AddressNumber", "StreetDirection", "StreetName", "HmAddress2", "City", "State", "Zip", "HmPhone", "UnLstHmPho?"]
30
+ assert_equal column_names, Family.column_names
31
+ end
32
+
33
+ def test_columns
34
+ column = Family.columns[2]
35
+ assert_kind_of Column, column
36
+ assert_respond_to column, 'type'
37
+ assert_respond_to column, 'name'
38
+ assert_respond_to column, 'size'
39
+ Family.columns.each do |c|
40
+ assert_not_nil c.name
41
+ assert_not_nil c.type
42
+ end
43
+ end
44
+
45
+ def test_column_klass
46
+ column = Family.columns[1]
47
+ assert_equal String, column.klass
48
+ Family.columns.each do |c|
49
+ assert_not_nil c.klass
50
+ assert_kind_of Class, c.klass
51
+ end
52
+ end
53
+
54
+
55
+ end
data/test/test_base.rb ADDED
@@ -0,0 +1,125 @@
1
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ require 'test/unit'
3
+ require File.join(File.dirname(__FILE__), 'test_helper')
4
+
5
+ class BaseTest < Test::Unit::TestCase
6
+
7
+ class NonExistentTable < ActiveMDB::Base
8
+ set_mdb_file TEST_DB
9
+ end
10
+ class Employee < ActiveMDB::Base
11
+ set_mdb_file TEST_DB
12
+ set_table_name 'Employee'
13
+ set_primary_key 'Emp_Id'
14
+ end
15
+ class Room < ActiveMDB::Base
16
+ set_mdb_file TEST_DB
17
+ set_table_name 'Room'
18
+ set_primary_key 'Room'
19
+ end
20
+
21
+ class Person < ActiveMDB::Base
22
+ set_mdb_file RETREAT_DB
23
+ set_table_name 'tblIndividData'
24
+ set_primary_key 'IndKey'
25
+ end
26
+
27
+
28
+ def test_setting_mdb_file
29
+ assert_equal TEST_DB, Employee.mdb_file
30
+ end
31
+
32
+ def test_mdb_creation
33
+ mdb = Employee.mdb
34
+ assert_not_nil mdb
35
+ assert_respond_to mdb, :tables
36
+ end
37
+
38
+ def test_get_table_name
39
+ assert_equal 'non_existent_tables', NonExistentTable.table_name
40
+ ActiveMDB::Base.pluralize_table_names = false
41
+ assert_equal 'non_existent_table', NonExistentTable.table_name
42
+ end
43
+
44
+ def test_set_table_name
45
+ Employee.set_table_name('foo')
46
+ assert_equal 'foo', Employee.table_name
47
+ assert_equal 'Room', Room.table_name
48
+ Employee.set_table_name('Employee')
49
+ end
50
+
51
+ def test_table_exists
52
+ assert !NonExistentTable.table_exists?
53
+ assert Room.table_exists?
54
+ end
55
+
56
+ def test_get_and_set_primary_key
57
+ assert_equal 'Emp_Id', Employee.primary_key
58
+ Employee.set_primary_key 'employee_id'
59
+ assert_equal 'employee_id', Employee.primary_key
60
+ end
61
+
62
+ def test_count
63
+ assert_equal 92, Room.count
64
+ assert_equal 53, Employee.count
65
+ end
66
+
67
+ def test_instantiate
68
+ hash = {"Department"=>"Engineering", "Gender"=>"M", "Room"=>"6072", "Title"=>"Programmer", "Emp_Id"=>"1045", "First_Name"=>"Robert", "Last_Name"=>"Weinfeld"}
69
+ record = Employee.send(:instantiate, hash)
70
+ methodized_hash = {}
71
+ hash.each {|k,v| methodized_hash[MDBTools.methodize(k)] = v}
72
+ assert_equal methodized_hash, record.instance_variable_get('@attributes')
73
+ hash = {"Area"=>"135.38","Entity_Handle"=>"9D7A","Room"=>"6004","Room_Type"=>"STOR-GEN"}
74
+ record = Room.send(:instantiate, hash)
75
+ assert_kind_of Float, record.instance_variable_get('@attributes')['area']
76
+ end
77
+
78
+ def test_find_from_hash
79
+ record = Employee.find_from_hash(:Room => '6072').first
80
+ assert_kind_of Employee, record
81
+ record = Room.find_first(:Area => 135.38)
82
+ assert_kind_of Room, record
83
+ end
84
+
85
+ def test_find_where
86
+ weinstein = Employee.find_where( "Last_Name = 'Weinfeld'").first
87
+ assert_equal 'Robert', weinstein.First_Name
88
+ end
89
+
90
+ def test_conditions_from_hash
91
+ hash = {:Room => '6072'}
92
+ conditions = "Room LIKE '%6072%'"
93
+ assert_equal conditions, Employee.conditions_from_hash(hash)
94
+ hash = {:Area => 45}
95
+ conditions = "Area = 45"
96
+ assert_equal conditions, Room.conditions_from_hash(hash)
97
+ end
98
+
99
+ def test_attribute_magic
100
+ record = Employee.find_from_hash(:Room => '6072').first
101
+ assert_equal '6072', record.Room
102
+ end
103
+
104
+
105
+ def test_find_all_with_arg
106
+ assert_equal 2, Employee.find_all(:First_Name => 'G').size
107
+ end
108
+
109
+ def test_find_all_without_arg
110
+ assert_nothing_raised { @employees = Employee.find_all }
111
+ assert_kind_of Employee, @employees.first
112
+ end
113
+
114
+ def test_column_for_field
115
+ assert_kind_of Column, Employee.column_for_field(:Room)
116
+ assert_nil Employee.column_for_field('foo')
117
+ end
118
+
119
+ def test_column_for_method
120
+ assert_kind_of Column, Employee.column_for_method(:room)
121
+ end
122
+
123
+
124
+
125
+ end
@@ -0,0 +1,26 @@
1
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ require 'test/unit'
3
+ require File.join(File.dirname(__FILE__), 'test_helper')
4
+
5
+ class ColumnTest < Test::Unit::TestCase
6
+
7
+
8
+
9
+ def setup
10
+ @c1 = Column.new('is_available', 'Boolean', 0)
11
+ end
12
+
13
+ def test_boolean_column
14
+ assert_equal false, @c1.type_cast(0)
15
+ end
16
+
17
+
18
+
19
+ # def test_string_to_time
20
+ # time = '05/18/76 00:00:00'
21
+ # t2 = "02/17/47 00:00:00"
22
+ # assert_equal 1976, Column.string_to_time(time).year
23
+ # assert_equal 5, Column.string_to_time(time).month
24
+ # end
25
+
26
+ end
@@ -0,0 +1,19 @@
1
+ require 'test/unit'
2
+ TEST_DB = File.join(File.dirname(__FILE__), '..', 'db', 'sample.mdb')
3
+ RETREAT_DB = File.join(File.dirname(__FILE__), '..', 'db', 'retreat.mdb')
4
+ NOT_A_DB = File.join(File.dirname(__FILE__), '..', 'db', 'not_an_mdb.txt')
5
+
6
+ class Test::Unit::TestCase
7
+
8
+
9
+
10
+ protected
11
+
12
+ def create_mdb(options={})
13
+ excluded = options[:exclude]
14
+ included = options[:include]
15
+ assert_nothing_raised { @db = MDB.new(TEST_DB, :exclude => excluded, :include => included ) }
16
+ assert_not_nil @db
17
+ end
18
+
19
+ end
data/test/test_mdb.rb ADDED
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ require 'test/unit'
3
+ require File.join(File.dirname(__FILE__), 'test_helper')
4
+
5
+ class MDBTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+
9
+ end
10
+
11
+ def test_table_exclusion
12
+ create_mdb(:exclude => ['Inventory'])
13
+ assert !@db.tables.include?('Inventory')
14
+ end
15
+
16
+ def test_table_inclusion
17
+ create_mdb(:include => ['Inventory', 'Room'])
18
+ assert_equal ['Inventory', 'Room'], @db.tables.sort
19
+ end
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+ end
28
+
@@ -0,0 +1,136 @@
1
+ require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ require 'test/unit'
3
+ require File.join(File.dirname(__FILE__), 'test_helper')
4
+
5
+
6
+ class MDBToolsTest < Test::Unit::TestCase
7
+ include MDBTools
8
+
9
+ EXCLUDE = ['Inventory']
10
+ TEST_TABLES = %w{ Room Computer Employee }
11
+ TORBATI_Y = {"Department"=>"Engineering",
12
+ "Gender"=>"F",
13
+ "Room"=>"6044",
14
+ "Title"=>"Programmer",
15
+ "Emp_Id"=>"1000",
16
+ "First_Name"=>"Yolanda",
17
+ "Last_Name"=>"Torbati"}
18
+ README = File.join(File.dirname(__FILE__), "..", "README")
19
+
20
+ def setup
21
+ @employees_csv = mdb_export(TEST_DB, 'Employee', :format => 'csv', :headers => true)
22
+ end
23
+
24
+ def test_check_file
25
+ assert_nothing_raised { check_file TEST_DB }
26
+ assert_raise(ArgumentError) { check_file 'completely_bogus_filename' }
27
+ # the README file is obviously not an Access database
28
+ assert_raise(ArgumentError) { check_file README}
29
+ end
30
+
31
+ def test_valid_file
32
+ assert valid_file?(TEST_DB)
33
+ assert !valid_file?(NOT_A_DB)
34
+ end
35
+
36
+ def test_mdb_version
37
+ assert_equal 'JET3', mdb_version(TEST_DB)
38
+ assert_equal '', mdb_version(NOT_A_DB)
39
+ end
40
+
41
+ def test_check_table
42
+ assert_nothing_raised { check_table TEST_DB, 'Employee'}
43
+ assert_raises(ArgumentError) { check_table TEST_DB, 'foobarbaz' }
44
+ end
45
+
46
+ def test_mdb_tables
47
+ tables1 = mdb_tables(TEST_DB, :exclude => EXCLUDE)
48
+ assert_equal TEST_TABLES, tables1
49
+
50
+ assert_raises( ArgumentError) { mdb_tables(TEST_DB, :include => [], :exclude => []) }
51
+ tables4 = mdb_tables(TEST_DB, :exclude => 'Room')
52
+ assert_equal %w{Computer Employee Inventory}, tables4
53
+ end
54
+
55
+ def test_mdb_tables_with_include
56
+ tables = mdb_tables(TEST_DB, :include => ['Room', 'Computer'])
57
+ assert_equal ['Room', 'Computer'], tables
58
+ end
59
+
60
+ def test_mdb_tables_with_nils
61
+ assert_nothing_raised do
62
+ tables = mdb_tables(TEST_DB, :include => nil, :exclude => nil)
63
+ assert_not_nil tables
64
+ end
65
+ end
66
+
67
+ def test_mdb_schema
68
+ assert_nothing_raised { @schema = mdb_schema(TEST_DB, 'Employee') }
69
+ assert_match /DROP TABLE/, @schema
70
+ end
71
+
72
+ def test_describe_table
73
+ descriptions = describe_table(TEST_DB, 'Employee')
74
+ assert_kind_of Array, descriptions
75
+ assert_kind_of Hash, descriptions.first
76
+ assert_equal 3, descriptions.first.size
77
+ assert_not_nil descriptions.first['Type']
78
+ end
79
+
80
+ def test_mdb_sql
81
+ result = [{"Department"=>"Human Resources", "Gender"=>"F", "Room"=>"6150", "Title"=>"Vice President", "Emp_Id"=>"1025", "First_Name"=>"Kathy", "Last_Name"=>"Ragerie"}]
82
+ assert_equal result, mdb_sql(TEST_DB, "select * from Employee where Room = '6150'")
83
+ bees = mdb_sql(TEST_DB, "select * from Employee where Last_Name like 'B%'")
84
+ assert_kind_of Array, bees
85
+ b = bees.first
86
+ assert_kind_of Hash, b
87
+ end
88
+
89
+ def test_field_names_for
90
+ fields = ["Last_Name","First_Name", "Gender", "Title", "Department", "Room", "Emp_Id"]
91
+ assert_equal fields, field_names_for(TEST_DB, 'Employee')
92
+ end
93
+
94
+
95
+ # def test_csv_to_hashes
96
+ # employee_hash = csv_to_hashes(@employees_csv)
97
+ # assert_equal TORBATI_Y, employee_hash.first
98
+ # end
99
+
100
+ def test_sql_select_where
101
+ yo = {"Department"=>"Engineering", "Gender"=>"F", "Room"=>"6044", "Title"=>"Programmer", "Emp_Id"=>"1000", "First_Name"=>"Yolanda", "Last_Name"=>"Torbati"}
102
+ assert_equal yo, sql_select_where(TEST_DB, 'Employee', ['*'], "First_Name LIKE 'Yolanda'" ).first
103
+ end
104
+
105
+ def test_compile_conditions
106
+ conditions = {:foo => 'bar', :baz => 1, :nark => 'noo'}
107
+ assert_equal "baz like '%1%' AND foo like '%bar%' AND nark like '%noo%'", compile_conditions(conditions)
108
+ end
109
+
110
+ def test_compiled_sql_select_where
111
+ sql_select_where(TEST_DB, 'Employee', nil, compile_conditions(:first_name => 'Yolanda'))
112
+ end
113
+
114
+ def test_count
115
+ assert_equal 92, faked_count(TEST_DB, 'Room', 'Room', nil)
116
+ end
117
+
118
+ def test_export_table_to_sql
119
+ # this test is dependent on specific content in the sample database
120
+ export = mdb_export(TEST_DB, 'Computer', :format => 'sql').split(LINEBREAK)
121
+ assert_equal 172, export.size
122
+ assert export.first =~ /INSERT INTO.*MITSUBISHI.*HL6605ATK/
123
+ end
124
+
125
+ def test_export_table_to_csv
126
+ export = mdb_export(TEST_DB, 'Employee', :format => 'csv').split(LINEBREAK)
127
+ assert_equal 53, export.size
128
+ assert export.first =~ /\"Torbati\",/
129
+ end
130
+
131
+ def test_backends
132
+ assert_nothing_raised { backends }
133
+ end
134
+
135
+
136
+ end
@@ -0,0 +1,24 @@
1
+ # require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ # require 'test/unit'
3
+ # require File.join(File.dirname(__FILE__), 'test_helper')
4
+ #
5
+ # class RecordTest < Test::Unit::TestCase
6
+ #
7
+ # def setup
8
+ # create_mdb
9
+ # @employee = @db.employee
10
+ # @line = ["Torbati","Yolanda", "F", "Programmer", "Engineering", "6044", "1000"]
11
+ # assert_nothing_raised { @record = Record.new(@employee, @line) }
12
+ # end
13
+ #
14
+ # def test_reflection
15
+ # assert_respond_to @record, 'gender'
16
+ # assert_respond_to @record, 'emp_id'
17
+ # assert_equal 'Yolanda', @record.first_name
18
+ # end
19
+ #
20
+ # def test_display
21
+ #
22
+ # end
23
+ #
24
+ # end
@@ -0,0 +1,50 @@
1
+ # require File.join(File.dirname(__FILE__), "..", 'lib', 'active_mdb')
2
+ # require 'test/unit'
3
+ # require File.join(File.dirname(__FILE__), 'test_helper')
4
+ #
5
+ # class TableTest < Test::Unit::TestCase
6
+ #
7
+ #
8
+ # def setup
9
+ # create_mdb
10
+ # @employee = @db.employee
11
+ # end
12
+ #
13
+ # def test_columns
14
+ # columns = @employee.columns
15
+ # assert_kind_of Array, columns
16
+ # assert_kind_of Column, columns.first
17
+ # assert_equal 7, columns.size
18
+ # end
19
+ #
20
+ # def test_create_record_struct
21
+ # assert_kind_of Class, @employee.record_struct
22
+ # members = @employee.record_struct.members
23
+ # assert_equal 7, members.size
24
+ # assert members.include?('emp_id')
25
+ # end
26
+ #
27
+ # def test_to_csv
28
+ # csv_text = @employee.to_csv
29
+ # assert_kind_of String, csv_text
30
+ # arrays = csv_text.split(MDBTools::LINEBREAK)
31
+ #
32
+ # # grab the headers and test for content
33
+ # assert arrays.shift.include?('Emp_Id')
34
+ # assert_equal 53, arrays.size
35
+ # end
36
+ #
37
+ # def test_find_first
38
+ # y = @employee.find_first(:first_name => 'Yolanda')
39
+ # assert_kind_of Record, y
40
+ # assert_equal 'Yolanda', y.first_name
41
+ # end
42
+ #
43
+ # def test_find_all
44
+ # a_names = @employee.find_all(:last_name => 'A')
45
+ # assert_kind_of Array, a_names
46
+ # assert_kind_of Record, a_names.first
47
+ # assert_equal 2, a_names.size
48
+ # end
49
+ #
50
+ # end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yob-activemdb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Matthew King
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-13 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: ActiveRecordy wrapper around MDB Tools, allowing POSIX platforms to read MS Access (.mdb) files
17
+ email: automatthew@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/active_mdb/mdb_tools.rb
26
+ - lib/active_mdb/column.rb
27
+ - lib/active_mdb/record.rb
28
+ - lib/active_mdb/table.rb
29
+ - lib/active_mdb/mdb.rb
30
+ - lib/active_mdb/base.rb
31
+ - lib/active_mdb.rb
32
+ - Rakefile
33
+ - README.txt
34
+ has_rdoc: true
35
+ homepage: http://activemdb.rubyforge.org/
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: ActiveRecordy wrapper around MDB Tools, allowing POSIX platforms to read MS Access (.mdb) files
60
+ test_files:
61
+ - test/test_column.rb
62
+ - test/test_record.rb
63
+ - test/test_activemdb.rb
64
+ - test/test_mdb_tools.rb
65
+ - test/test_table.rb
66
+ - test/test_mdb.rb
67
+ - test/test_base.rb
68
+ - test/test_helper.rb