rbase 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +26 -0
- data/lib/rbase.rb +6 -0
- data/lib/rbase/builder.rb +31 -0
- data/lib/rbase/columns.rb +192 -0
- data/lib/rbase/record.rb +107 -0
- data/lib/rbase/schema.rb +52 -0
- data/lib/rbase/schema_dumper.rb +29 -0
- data/lib/rbase/table.rb +204 -0
- metadata +54 -0
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
task :default => ['gem']
|
5
|
+
|
6
|
+
desc 'Create a gem'
|
7
|
+
task :gem do
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
s.name = 'rbase'
|
10
|
+
s.version = '0.1'
|
11
|
+
s.summary = 'Library to create/read/write to XBase databases (*.DBF files)'
|
12
|
+
s.files = Dir.glob('**/*').delete_if { |item| item.include?('.svn') }
|
13
|
+
s.require_path = 'lib'
|
14
|
+
s.autorequire = 'rbase'
|
15
|
+
s.authors = 'Maxim Kulkin, Leonardo Augusto Pires'
|
16
|
+
s.email = 'maxim.kulkin@gmail.com, leonardo.pires@gmail.com'
|
17
|
+
s.homepage = 'http://rbase.rubyforge.com/'
|
18
|
+
s.rubyforge_project = 'rbase'
|
19
|
+
s.has_rdoc = true
|
20
|
+
|
21
|
+
s.required_ruby_version = '>= 1.8.2'
|
22
|
+
end
|
23
|
+
|
24
|
+
Gem.manage_gems
|
25
|
+
Gem::Builder.new(spec).build
|
26
|
+
end
|
data/lib/rbase.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rbase/schema'
|
2
|
+
|
3
|
+
module RBase
|
4
|
+
LANGUAGE_RUSSIAN_DOS = 0x66
|
5
|
+
LANGUAGE_RUSSIAN_WINDOWS = 0xc9
|
6
|
+
|
7
|
+
# Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
|
8
|
+
#
|
9
|
+
# For list of available options see Table::create documentation.
|
10
|
+
#
|
11
|
+
# == Example
|
12
|
+
#
|
13
|
+
# XBase.create_table 'people' do |t|
|
14
|
+
# t.column :name, :string, :size => 30
|
15
|
+
# t.column :birthdate, :date
|
16
|
+
# t.column :active, :boolean
|
17
|
+
# t.column :tax, :integer, :size => 10, :decimal => 2
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# For documentation on column parameters see XBase::Schema.column documentation.
|
21
|
+
#
|
22
|
+
def self.create_table(name, options = {})
|
23
|
+
options[:language] ||= LANGUAGE_RUSSIAN_WINDOWS
|
24
|
+
|
25
|
+
schema = Schema.new
|
26
|
+
yield schema if block_given?
|
27
|
+
|
28
|
+
Table.create name, schema, options
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module RBase
|
2
|
+
module Columns
|
3
|
+
|
4
|
+
# Base class for all column types
|
5
|
+
class Column
|
6
|
+
@@types = {}
|
7
|
+
|
8
|
+
# Assigns column type string to current class
|
9
|
+
def self.column_type(type)
|
10
|
+
@type = type
|
11
|
+
@@types[type] = self
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns column type class that correspond to given column type string
|
15
|
+
def self.column_for(type)
|
16
|
+
throw "Unknown column type '#{type}'" unless @@types.has_key?(type)
|
17
|
+
@@types[type]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns column type as 1 character string
|
21
|
+
def self.type
|
22
|
+
@type
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns column type as 1 character string
|
26
|
+
def type
|
27
|
+
self.class.type
|
28
|
+
end
|
29
|
+
|
30
|
+
# Column name
|
31
|
+
attr_reader :name
|
32
|
+
|
33
|
+
# Column offset from the beginning of the record
|
34
|
+
attr_reader :offset
|
35
|
+
|
36
|
+
# Column size in characters
|
37
|
+
attr_reader :size
|
38
|
+
|
39
|
+
# Number of decimal places
|
40
|
+
attr_reader :decimal
|
41
|
+
|
42
|
+
def initialize(name, options = {})
|
43
|
+
options.merge({:name => name, :type => self.class.type}).each { |k, v| self.instance_variable_set("@#{k}", v) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Packs column value for storing it in XBase file.
|
47
|
+
def pack(value)
|
48
|
+
throw "Not implemented"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Unpacks stored in XBase column data into appropriate Ruby form.
|
52
|
+
def unpack(value)
|
53
|
+
throw "Not implemented"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
class CharacterColumn < Column
|
59
|
+
column_type 'C'
|
60
|
+
|
61
|
+
def initialize(name, options = {})
|
62
|
+
if options[:size] && options[:decimal]
|
63
|
+
size = options[:decimal]*256 + options[:size]
|
64
|
+
else
|
65
|
+
size = options[:size] || 254
|
66
|
+
end
|
67
|
+
|
68
|
+
super name, options.merge(:size => size)
|
69
|
+
end
|
70
|
+
|
71
|
+
def pack(value)
|
72
|
+
[value].pack("A#{size}")
|
73
|
+
end
|
74
|
+
|
75
|
+
def unpack(data)
|
76
|
+
data.rstrip
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class NumberColumn < Column
|
82
|
+
column_type 'N'
|
83
|
+
|
84
|
+
def initialize(name, options = {})
|
85
|
+
size = options[:size] || 18
|
86
|
+
size = 18 if size > 18
|
87
|
+
|
88
|
+
super name, options.merge(:size => size)
|
89
|
+
end
|
90
|
+
|
91
|
+
def pack(value)
|
92
|
+
if decimal && decimal!=0
|
93
|
+
[format("%-#{size-decimal-1}.#{decimal}f", value)].pack("A#{size}")
|
94
|
+
else
|
95
|
+
[format("%-#{size}d", value)].pack("A#{size}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def unpack(data)
|
100
|
+
return nil if data.rstrip == ''
|
101
|
+
data.rstrip.to_i
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
class LogicalColumn < Column
|
107
|
+
column_type 'L'
|
108
|
+
|
109
|
+
def initialize(name, options = {})
|
110
|
+
super name, options.merge(:size => 1)
|
111
|
+
end
|
112
|
+
|
113
|
+
def pack(value)
|
114
|
+
case value
|
115
|
+
when true then 'T'
|
116
|
+
when false then 'F'
|
117
|
+
else '?'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def unpack(data)
|
122
|
+
case data.upcase
|
123
|
+
when 'Y', 'T'
|
124
|
+
true
|
125
|
+
when 'N', 'F'
|
126
|
+
false
|
127
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
class DateColumn < Column
|
135
|
+
column_type 'D'
|
136
|
+
|
137
|
+
def initialize(name, options = {})
|
138
|
+
super name, options.merge(:size => 8)
|
139
|
+
end
|
140
|
+
|
141
|
+
def pack(value)
|
142
|
+
value ? value.strftime('%Y%m%d'): ' '*8
|
143
|
+
end
|
144
|
+
|
145
|
+
def unpack(data)
|
146
|
+
return nil if data.rstrip == ''
|
147
|
+
Date.new(*data.unpack("a4a2a2").map { |s| s.to_i})
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
class MemoColumn < Column
|
153
|
+
column_type 'M'
|
154
|
+
|
155
|
+
def initialize(name, options = {})
|
156
|
+
super name, options.merge(:size => 10)
|
157
|
+
end
|
158
|
+
|
159
|
+
def pack(value)
|
160
|
+
# Not implemented yet.. using stub
|
161
|
+
[].pack('A10')
|
162
|
+
end
|
163
|
+
|
164
|
+
def unpack(data)
|
165
|
+
# Not implemented yet.. using stub
|
166
|
+
''
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
class FloatColumn < Column
|
172
|
+
column_type 'F'
|
173
|
+
|
174
|
+
def initialize(name, options = {})
|
175
|
+
super name, options.merge(:size => 20)
|
176
|
+
end
|
177
|
+
|
178
|
+
def decimal
|
179
|
+
(@decimal && @decimal <= 15) ? @decimal : 2
|
180
|
+
end
|
181
|
+
|
182
|
+
def pack(value)
|
183
|
+
[format("%-#{size-decimal-1}.#{decimal}f", value)].pack("A#{size}")
|
184
|
+
end
|
185
|
+
|
186
|
+
def unpack(data)
|
187
|
+
data.rstrip.to_f
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
data/lib/rbase/record.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module RBase
|
2
|
+
|
3
|
+
# Class that contains data for particular table row.
|
4
|
+
# Should not be created explicitly (use Table#create to create records)
|
5
|
+
#
|
6
|
+
# == Accessing attributes
|
7
|
+
#
|
8
|
+
# You can read and assign values to row's columns using simple property syntax:
|
9
|
+
#
|
10
|
+
# user = users_table[0]
|
11
|
+
# user.name = 'Bob'
|
12
|
+
# user.birth_date = Date.new(1980, 2, 29)
|
13
|
+
# user.save
|
14
|
+
#
|
15
|
+
# puts user.name
|
16
|
+
# puts user.birth_date
|
17
|
+
#
|
18
|
+
class Record
|
19
|
+
attr_reader :table, :index
|
20
|
+
|
21
|
+
def initialize(table)
|
22
|
+
@table = table
|
23
|
+
@values_cached = {}
|
24
|
+
@values_changed = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def load(index, data)
|
30
|
+
@table = table
|
31
|
+
@index = index
|
32
|
+
@data = data.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def create(attributes = {})
|
36
|
+
attributes.each { |k, v| @values_changed[k.to_s.upcase] = v }
|
37
|
+
end
|
38
|
+
|
39
|
+
public
|
40
|
+
|
41
|
+
# Returns true if record was never saved to database; otherwise return false.
|
42
|
+
def new_record?
|
43
|
+
@data.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Save record to database.
|
47
|
+
def save
|
48
|
+
record = self
|
49
|
+
@table.instance_eval { save(record) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Delete record from database.
|
53
|
+
def delete
|
54
|
+
@deleted = true
|
55
|
+
save
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns true if record was marked as deleted; otherwise return false.
|
59
|
+
def deleted?
|
60
|
+
@deleted ||= new_record? ? false : @data[0, 1] == '*'
|
61
|
+
end
|
62
|
+
|
63
|
+
def method_missing(sym, *args)
|
64
|
+
name = sym.to_s
|
65
|
+
if /=$/ =~ name && args.size == 1
|
66
|
+
set_value(name[0..-2], args.first)
|
67
|
+
else
|
68
|
+
get_value(name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def serialize
|
73
|
+
if new_record?
|
74
|
+
@data = deleted? ? '*' : ' '
|
75
|
+
@data << @table.columns.collect do |column|
|
76
|
+
column.pack(@values_changed[column.name])
|
77
|
+
end.join
|
78
|
+
else
|
79
|
+
@data[0, 1] = deleted? ? '*' : ' '
|
80
|
+
values_changed.each do |k, v|
|
81
|
+
column = @table.column(name)
|
82
|
+
@data[column.offset, column.size] = column.pack(value)
|
83
|
+
values_cached[k] = v
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@data
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
# Returns value of specified column
|
92
|
+
def get_value(name)
|
93
|
+
name = name.to_s.upcase.to_sym
|
94
|
+
return @values_changed[name] if @values_changed.has_key?(name)
|
95
|
+
return nil if new_record?
|
96
|
+
column = @table.column(name)
|
97
|
+
@values_cached[name] ||= column.unpack(@data[column.offset, column.size])
|
98
|
+
end
|
99
|
+
|
100
|
+
# Sets value of specified column.
|
101
|
+
def set_value(name, value)
|
102
|
+
name = name.to_s.upcase.to_sym
|
103
|
+
@values_changed[name] = value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
data/lib/rbase/schema.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rbase/columns'
|
2
|
+
|
3
|
+
module RBase
|
4
|
+
|
5
|
+
class Schema
|
6
|
+
# Returns list of all columns defined.
|
7
|
+
attr_reader :columns
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@columns = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Declares new column.
|
14
|
+
#
|
15
|
+
# Options:
|
16
|
+
#
|
17
|
+
# * :size - size of the column in characters
|
18
|
+
# * :decimal - number of decimal positions
|
19
|
+
#
|
20
|
+
# There are column types that require it's size to be specified (but they still have reasonable defaults).
|
21
|
+
# But some column types (e.g. :date type) have fixes size that cannot be overriden.
|
22
|
+
#
|
23
|
+
# There are several column types available:
|
24
|
+
#
|
25
|
+
# * :string - corresponds to fixed length character column. Column size is limited to 254 (default).
|
26
|
+
# * :date - date column type
|
27
|
+
# * :boolean - logical column type
|
28
|
+
# * :integer - number column type. Number is stored in human readable form
|
29
|
+
# (text representation), so you should specify it's size in characters. Maximum column size is 18 (default).
|
30
|
+
# If :decimal option not equal to 0, number contains <:decimal> fraction positions.
|
31
|
+
# You should adjust :size keeping :decimal positions + 1 (for decimal point) in mind.
|
32
|
+
# * :memo - memo column. Memo is a text field that can be more than 254 chars long. Memo data is stored in separate file.
|
33
|
+
# This column type is not yet supported.
|
34
|
+
#
|
35
|
+
#
|
36
|
+
def column(name, type, options = {})
|
37
|
+
name = name.to_s.upcase
|
38
|
+
case type
|
39
|
+
when :string then type = 'C'
|
40
|
+
when :integer then type = 'N'
|
41
|
+
when :float then
|
42
|
+
type = 'N'
|
43
|
+
options[:decimal] ||= 6
|
44
|
+
when :boolean then type = 'L'
|
45
|
+
when :date then type = 'D'
|
46
|
+
end
|
47
|
+
|
48
|
+
@columns << Columns::Column.column_for(type).new(name, options)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RBase
|
2
|
+
|
3
|
+
class SchemaDumper
|
4
|
+
# Produce ruby schema for a given table.
|
5
|
+
#
|
6
|
+
# Parameters
|
7
|
+
# table - instance of XBase::Table opened
|
8
|
+
#
|
9
|
+
# == Example
|
10
|
+
#
|
11
|
+
# users = XBase::Table.open('users')
|
12
|
+
# File.open('users.dump.rb', 'w') do |f|
|
13
|
+
# f.write XBase::SchemaDumper.dump(users)
|
14
|
+
# end
|
15
|
+
# users.close
|
16
|
+
#
|
17
|
+
def self.dump(table)
|
18
|
+
output = ''
|
19
|
+
output << "XBase.create_table :#{table.name} do |t|\n"
|
20
|
+
|
21
|
+
table.columns.each do |column|
|
22
|
+
output << " t.column '#{column.name}', '#{column.type}', :size => #{column.size}#{ (column.decimal && column.decimal > 0) ? ", :decimal => #{column.decimal}" : ''}\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
output << "end\n"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/rbase/table.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
module RBase
|
2
|
+
|
3
|
+
class Table
|
4
|
+
private_class_method :new
|
5
|
+
|
6
|
+
# Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
|
7
|
+
#
|
8
|
+
# Allowed options
|
9
|
+
# * :language - language character set used in database. Can be one of LANGUAGE_* constants
|
10
|
+
def self.create(name, schema, options = {})
|
11
|
+
date = Date.today
|
12
|
+
|
13
|
+
record_size = 1+schema.columns.inject(0) { |size, column| size + column.size }
|
14
|
+
|
15
|
+
data = ''
|
16
|
+
data << [0xf5].pack('C') # version
|
17
|
+
#data << [0x3].pack('C') # version
|
18
|
+
data << [date.year % 100, date.month, date.day].pack('CCC') # last modification date
|
19
|
+
data << [0].pack('L') # number of records
|
20
|
+
# data << [32+schema.columns.size*32+263+1].pack('v') # data size
|
21
|
+
data << [32+schema.columns.size*32+1].pack('v') # data size
|
22
|
+
data << [record_size].pack('v') # record size
|
23
|
+
data << [].pack('x2') # reserved
|
24
|
+
data << [].pack('x') # incomplete transaction
|
25
|
+
data << [].pack('x') # encyption flag
|
26
|
+
data << [].pack('x4') # reserved
|
27
|
+
data << [].pack('x8') # reserved
|
28
|
+
data << [0].pack('c') # mdx flag
|
29
|
+
data << [options[:language]].pack('C') # language driver
|
30
|
+
data << [].pack('x2') # reserved
|
31
|
+
|
32
|
+
offset = 1 # take into account 1 byte for deleted flag
|
33
|
+
data << schema.columns.collect do |column|
|
34
|
+
s = ''
|
35
|
+
s << [column.name.to_s[0..9]].pack('a11') # field name
|
36
|
+
s << [column.type].pack('a') # field type
|
37
|
+
s << [offset].pack('L') # field data offset
|
38
|
+
s << [column.size].pack('C') # field size
|
39
|
+
s << [column.decimal || 0].pack('C') # decimal count
|
40
|
+
s << [].pack('x2') # reserved
|
41
|
+
s << [].pack('x') # work area id
|
42
|
+
s << [].pack('x2') # reserved
|
43
|
+
s << [].pack('x') # flag for SET FIELDS
|
44
|
+
s << [].pack('x7') # reserved
|
45
|
+
s << [].pack('x') # index field flag
|
46
|
+
offset += column.size
|
47
|
+
s
|
48
|
+
end.join
|
49
|
+
|
50
|
+
data << [13].pack('C') # terminator
|
51
|
+
|
52
|
+
data << [26].pack('C') # end of file
|
53
|
+
|
54
|
+
File.open("#{name}.dbf", 'wb') do |f|
|
55
|
+
f.write data
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Open table with given name.
|
60
|
+
# Table name should be like file name without ".dbf" suffix.
|
61
|
+
def self.open(name)
|
62
|
+
table = new
|
63
|
+
table.instance_eval { open("#{name}.dbf") }
|
64
|
+
table
|
65
|
+
end
|
66
|
+
|
67
|
+
# Physically remove records that were marked as deleted from file.
|
68
|
+
def pack
|
69
|
+
packed_count = 0
|
70
|
+
count.times do |i|
|
71
|
+
@file.pos = @record_offset + @record_size*i
|
72
|
+
data = @file.read(@record_size)
|
73
|
+
unless data[0, 1]=='*'
|
74
|
+
if i!=packed_count
|
75
|
+
@file.pos = @record_offset + @record_size*packed_count
|
76
|
+
@file.write data
|
77
|
+
end
|
78
|
+
packed_count += 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
file_end = @record_offset + @record_size*packed_count
|
83
|
+
@file.pos = file_end
|
84
|
+
@file.write "\x1a"
|
85
|
+
@file.truncate file_end+1
|
86
|
+
|
87
|
+
self.count = packed_count
|
88
|
+
update_header
|
89
|
+
end
|
90
|
+
|
91
|
+
def close
|
92
|
+
@file.close
|
93
|
+
end
|
94
|
+
|
95
|
+
attr_reader :name, :count, :columns, :last_modified_on, :language
|
96
|
+
|
97
|
+
# Return instance of XBase::Column for given column name
|
98
|
+
def column(name)
|
99
|
+
@name_to_columns[name]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create new record and populate it with given attributes
|
103
|
+
def create(attributes = {})
|
104
|
+
record = Record.new(self)
|
105
|
+
record.instance_eval { create(attributes) }
|
106
|
+
record
|
107
|
+
end
|
108
|
+
|
109
|
+
# Load record stored in position 'index'
|
110
|
+
def load(index)
|
111
|
+
@file.pos = @record_offset + @record_size*index
|
112
|
+
data = @file.read(@record_size)
|
113
|
+
record = Record.new(self)
|
114
|
+
record.instance_eval { load(index, data) }
|
115
|
+
record
|
116
|
+
end
|
117
|
+
|
118
|
+
alias_method :[], :load
|
119
|
+
|
120
|
+
def []=(index, record)
|
121
|
+
record.instance_eval { @index = index }
|
122
|
+
save(record)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Iterate through all (even deleted) records
|
126
|
+
def each_with_deleted
|
127
|
+
return unless block_given?
|
128
|
+
|
129
|
+
count.times do |i|
|
130
|
+
yield load(i)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Iterate through all non-deleted records
|
135
|
+
def each
|
136
|
+
return unless block_given?
|
137
|
+
|
138
|
+
self.each_with_deleted do |record|
|
139
|
+
yield record unless record.deleted?
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def open(name)
|
146
|
+
@name = File.basename(name, '.dbf')
|
147
|
+
@file = File.open(name, "r+b")
|
148
|
+
header = @file.read(32)
|
149
|
+
|
150
|
+
year, month, day = *header.unpack('@1ccc')
|
151
|
+
year += 2000 if year >= 100
|
152
|
+
|
153
|
+
@last_modified_on = Date.new(year, month, day)
|
154
|
+
@count = header.unpack('@4V').first
|
155
|
+
@language = header.unpack('@29c').first
|
156
|
+
|
157
|
+
@record_offset = *header.unpack('@8v')
|
158
|
+
@record_size = *header.unpack('@10v')
|
159
|
+
|
160
|
+
@file.pos = 32
|
161
|
+
|
162
|
+
@columns = []
|
163
|
+
@name_to_columns = {}
|
164
|
+
while true do
|
165
|
+
column_data = @file.read(32)
|
166
|
+
break if column_data[0, 1] == "\x0d"
|
167
|
+
name, type, offset, size, decimal = *column_data.unpack('@0a11aLCC')
|
168
|
+
name = name.strip
|
169
|
+
@columns << Columns::Column.column_for(type).new(name, :offset => offset, :size => size, :decimal => decimal)
|
170
|
+
@name_to_columns[name.upcase.to_sym] = @columns.last
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def save(record)
|
175
|
+
if !record.index
|
176
|
+
@file.pos = @record_offset + @record_size*count
|
177
|
+
@file.write record.serialize
|
178
|
+
@file.write [26].pack('c')
|
179
|
+
self.count += 1
|
180
|
+
else
|
181
|
+
throw "Index out of bound" if index>=count
|
182
|
+
@file.pos = @record_offset + @record_size*index
|
183
|
+
@file.write record.serialize
|
184
|
+
end
|
185
|
+
update_header
|
186
|
+
end
|
187
|
+
|
188
|
+
def count=(value)
|
189
|
+
@count = value
|
190
|
+
end
|
191
|
+
|
192
|
+
def last_modified_on=(value)
|
193
|
+
@last_modified_on = value
|
194
|
+
end
|
195
|
+
|
196
|
+
def update_header
|
197
|
+
@last_modified_on = Date.today
|
198
|
+
@file.pos = 1
|
199
|
+
@file.write([last_modified_on.year % 100, last_modified_on.month, last_modified_on.day].pack('ccc'))
|
200
|
+
@file.write([count].pack('V'))
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: rbase
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.1"
|
7
|
+
date: 2006-09-11 00:00:00 +04:00
|
8
|
+
summary: Library to create/read/write to XBase databases (*.DBF files)
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: maxim.kulkin@gmail.com, leonardo.pires@gmail.com
|
12
|
+
homepage: http://rbase.rubyforge.com/
|
13
|
+
rubyforge_project: rbase
|
14
|
+
description:
|
15
|
+
autorequire: rbase
|
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: 1.8.2
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Maxim Kulkin, Leonardo Augusto Pires
|
30
|
+
files:
|
31
|
+
- Rakefile
|
32
|
+
- lib
|
33
|
+
- lib/rbase
|
34
|
+
- lib/rbase.rb
|
35
|
+
- lib/rbase/schema_dumper.rb
|
36
|
+
- lib/rbase/record.rb
|
37
|
+
- lib/rbase/columns.rb
|
38
|
+
- lib/rbase/schema.rb
|
39
|
+
- lib/rbase/builder.rb
|
40
|
+
- lib/rbase/table.rb
|
41
|
+
test_files: []
|
42
|
+
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
extra_rdoc_files: []
|
46
|
+
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
dependencies: []
|
54
|
+
|