rbase-ff 0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +27 -0
- data/lib/rbase/builder.rb +34 -0
- data/lib/rbase/columns.rb +251 -0
- data/lib/rbase/encoder.rb +11 -0
- data/lib/rbase/memo_file.rb +63 -0
- data/lib/rbase/record.rb +141 -0
- data/lib/rbase/schema.rb +52 -0
- data/lib/rbase/schema_dumper.rb +29 -0
- data/lib/rbase/table.rb +239 -0
- data/lib/rbase.rb +8 -0
- data/rbase.gemspec +13 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 43fb9c0a12fadbc618c2447b8a3d9f67e30cc4b0
|
4
|
+
data.tar.gz: 8b9e1657d06949598eb524d32d844c7bf1a0e773
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0b6f2fc63244d75a344b963ce63a9b03b1b446db53fc5e72144770a8fd01f855e64c49f8817b62cfa3541b369fe13c3da31f5fd184b7129f4650652fba2c1b75
|
7
|
+
data.tar.gz: 2d540336e920c3e05b286d452afcfbaf07989f66944edb5d3e023186f8396f4b82f0608e0cae12e22c5c2b3725b333d5f2d40d3879aac9c71d24f490fe6f71b6
|
data/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
rbase
|
2
|
+
=====
|
3
|
+
|
4
|
+
rbase gem (working with dbf files) for ruby 1.9.3 (iconv removed, some fixes)
|
5
|
+
|
6
|
+
How to use
|
7
|
+
==========
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'rubygems'
|
11
|
+
require "date"
|
12
|
+
require "rbase"
|
13
|
+
|
14
|
+
RBase.create_table 'people' do |t|
|
15
|
+
t.column :name, :string, :size => 30
|
16
|
+
t.column :birthdate, :date
|
17
|
+
t.column :active, :boolean
|
18
|
+
t.column :tax, :integer, :size => 10, :decimal => 2
|
19
|
+
end
|
20
|
+
|
21
|
+
RBase::Table.open 'people' do |t|
|
22
|
+
t.create name: 'People-1', birthdate: Date.new(2017,1,2), active: true, tax: 5.2
|
23
|
+
t.create name: 'People-2', birthdate: Date.new(2018,1,2), active: true, tax: 6.2
|
24
|
+
t.create name: 'People-3', birthdate: Date.new(2019,1,2), active: true, tax: 7.2
|
25
|
+
t.create name: 'People-4', birthdate: Date.new(2020,1,2), active: true, tax: 8.2
|
26
|
+
end
|
27
|
+
```
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rbase/schema'
|
2
|
+
|
3
|
+
module RBase
|
4
|
+
LANGUAGE_US_DOS = 0x01
|
5
|
+
LANGUAGE_INTL_DOS = 0x02
|
6
|
+
LANGUAGE_ANSI_WINDOWS = 0x03
|
7
|
+
LANGUAGE_RUSSIAN_DOS = 0x66
|
8
|
+
LANGUAGE_RUSSIAN_WINDOWS = 0xc9
|
9
|
+
|
10
|
+
# Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
|
11
|
+
#
|
12
|
+
# For list of available options see Table::create documentation.
|
13
|
+
#
|
14
|
+
# == Example
|
15
|
+
#
|
16
|
+
# RBase.create_table 'people' do |t|
|
17
|
+
# t.column :name, :string, :size => 30
|
18
|
+
# t.column :birthdate, :date
|
19
|
+
# t.column :active, :boolean
|
20
|
+
# t.column :tax, :integer, :size => 10, :decimal => 2
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# For documentation on column parameters see RBase::Schema.column documentation.
|
24
|
+
#
|
25
|
+
def self.create_table(name, options = {})
|
26
|
+
options[:language] ||= LANGUAGE_RUSSIAN_WINDOWS
|
27
|
+
|
28
|
+
schema = Schema.new
|
29
|
+
yield schema if block_given?
|
30
|
+
|
31
|
+
Table.create name, schema, options
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,251 @@
|
|
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
|
+
|
47
|
+
def attach_to(table)
|
48
|
+
@table = table
|
49
|
+
end
|
50
|
+
|
51
|
+
# Packs column value for storing it in XBase file.
|
52
|
+
def pack(value)
|
53
|
+
throw "Not implemented"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Unpacks stored in XBase column data into appropriate Ruby form.
|
57
|
+
def unpack(value)
|
58
|
+
throw "Not implemented"
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
"#{name}(type=#{type}, size=#{size})"
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def table
|
68
|
+
@table
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
class CharacterColumn < Column
|
74
|
+
column_type 'C'
|
75
|
+
|
76
|
+
def initialize(name, options = {})
|
77
|
+
if options[:size] && options[:decimal]
|
78
|
+
size = options[:decimal]*256 + options[:size]
|
79
|
+
else
|
80
|
+
size = options[:size] || 254
|
81
|
+
end
|
82
|
+
|
83
|
+
super name, options.merge(:size => size)
|
84
|
+
|
85
|
+
if options[:encoding]
|
86
|
+
@unpack_converter = Encoder.new(options[:encoding], 'utf-8')
|
87
|
+
@pack_converter = Encoder.new('utf-8', options[:encoding])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def pack(value)
|
92
|
+
value = value.to_s
|
93
|
+
value = @pack_converter.en(value) if @pack_converter
|
94
|
+
[value].pack("A#{size}")
|
95
|
+
end
|
96
|
+
|
97
|
+
def unpack(data)
|
98
|
+
value = data.rstrip
|
99
|
+
value = @unpack_converter.en(value) if @unpack_converter
|
100
|
+
value
|
101
|
+
end
|
102
|
+
|
103
|
+
def inspect
|
104
|
+
"#{name}(string #{size})"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
class NumberColumn < Column
|
110
|
+
column_type 'N'
|
111
|
+
|
112
|
+
def initialize(name, options = {})
|
113
|
+
size = options[:size] || 18
|
114
|
+
size = 18 if size > 18
|
115
|
+
|
116
|
+
super name, options.merge(:size => size)
|
117
|
+
end
|
118
|
+
|
119
|
+
def pack(value)
|
120
|
+
if value
|
121
|
+
if float?
|
122
|
+
[format("%#{size-decimal-1}.#{decimal}f", value)].pack("A#{size}")
|
123
|
+
else
|
124
|
+
[format("%#{size}d", value)].pack("A#{size}")
|
125
|
+
end
|
126
|
+
else
|
127
|
+
" "*size
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def unpack(data)
|
132
|
+
return nil if data.strip == ''
|
133
|
+
data.rstrip.to_i
|
134
|
+
end
|
135
|
+
|
136
|
+
def inspect
|
137
|
+
if float?
|
138
|
+
"#{name}(decimal)"
|
139
|
+
else
|
140
|
+
"#{name}(integer)"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def float?
|
145
|
+
decimal && decimal != 0
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
class LogicalColumn < Column
|
151
|
+
column_type 'L'
|
152
|
+
|
153
|
+
def initialize(name, options = {})
|
154
|
+
super name, options.merge(:size => 1)
|
155
|
+
end
|
156
|
+
|
157
|
+
def pack(value)
|
158
|
+
case value
|
159
|
+
when true then 'T'
|
160
|
+
when false then 'F'
|
161
|
+
else '?'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def unpack(data)
|
166
|
+
case data.upcase
|
167
|
+
when 'Y', 'T'
|
168
|
+
true
|
169
|
+
when 'N', 'F'
|
170
|
+
false
|
171
|
+
else
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def inspect
|
177
|
+
"#{name}(boolean)"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
class DateColumn < Column
|
183
|
+
column_type 'D'
|
184
|
+
|
185
|
+
def initialize(name, options = {})
|
186
|
+
super name, options.merge(:size => 8)
|
187
|
+
end
|
188
|
+
|
189
|
+
def pack(value)
|
190
|
+
value ? value.strftime('%Y%m%d'): ' '*8
|
191
|
+
end
|
192
|
+
|
193
|
+
def unpack(data)
|
194
|
+
return nil if data.rstrip == ''
|
195
|
+
Date.new(*data.unpack("a4a2a2").map { |s| s.to_i})
|
196
|
+
end
|
197
|
+
|
198
|
+
def inspect
|
199
|
+
"#{name}(date)"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
class MemoColumn < Column
|
205
|
+
column_type 'M'
|
206
|
+
|
207
|
+
def initialize(name, options = {})
|
208
|
+
super name, options.merge(:size => 10)
|
209
|
+
end
|
210
|
+
|
211
|
+
def pack(value)
|
212
|
+
packed_value = table.memo.write(value)
|
213
|
+
[format("%-10d", packed_value)].pack('A10')
|
214
|
+
end
|
215
|
+
|
216
|
+
def unpack(data)
|
217
|
+
table.memo.read(data.to_i)
|
218
|
+
end
|
219
|
+
|
220
|
+
def inspect
|
221
|
+
"#{name}(memo)"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
class FloatColumn < Column
|
227
|
+
column_type 'F'
|
228
|
+
|
229
|
+
def initialize(name, options = {})
|
230
|
+
super name, options.merge(:size => 20)
|
231
|
+
end
|
232
|
+
|
233
|
+
def decimal
|
234
|
+
(@decimal && @decimal <= 15) ? @decimal : 2
|
235
|
+
end
|
236
|
+
|
237
|
+
def pack(value)
|
238
|
+
[format("%-#{size-decimal-1}.#{decimal}f", value || 0.0)].pack("A#{size}")
|
239
|
+
end
|
240
|
+
|
241
|
+
def unpack(data)
|
242
|
+
data.rstrip.to_f
|
243
|
+
end
|
244
|
+
|
245
|
+
def inspect
|
246
|
+
"#{name}(float)"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module RBase
|
2
|
+
module MemoFile
|
3
|
+
|
4
|
+
class DummyMemoFile
|
5
|
+
def read(index)
|
6
|
+
''
|
7
|
+
end
|
8
|
+
|
9
|
+
def write(value)
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DBase3MemoFile
|
15
|
+
HEADER_SIZE = 512
|
16
|
+
BLOCK_SIZE = 512
|
17
|
+
BLOCK_TERMINATOR = "\x1a\x1a"
|
18
|
+
|
19
|
+
def initialize(name)
|
20
|
+
@file = File.open(name)
|
21
|
+
|
22
|
+
@header = @file.read(HEADER_SIZE)
|
23
|
+
@next_block = header.unpack('@0L')
|
24
|
+
@version = header.unpack('@16c')
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(index)
|
28
|
+
@file.pos = index*BLOCK_SIZE + HEADER_SIZE
|
29
|
+
|
30
|
+
result = ''
|
31
|
+
loop do
|
32
|
+
data = @file.read(BLOCK_SIZE)
|
33
|
+
terminator_pos = data.index(BLOCK_TERMINATOR)
|
34
|
+
if terminator_pos
|
35
|
+
break result + data[0, terminator_pos]
|
36
|
+
end
|
37
|
+
result += data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def write(value)
|
42
|
+
@file.pos = @next_block*BLOCK_SIZE + HEADER_SIZE
|
43
|
+
value += BLOCK_TERMINATOR
|
44
|
+
blocks_num = (value.length+511)/512
|
45
|
+
@file.write [value].pack("a#{512*blocks_num}")
|
46
|
+
|
47
|
+
position = @next_block
|
48
|
+
@next_block += blocks_num
|
49
|
+
update_header
|
50
|
+
|
51
|
+
position
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def update_header
|
57
|
+
@file.pos = 0
|
58
|
+
@file.write [@next_block].pack("L")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
data/lib/rbase/record.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
module RBase
|
2
|
+
|
3
|
+
class StandardError < Exception; end
|
4
|
+
|
5
|
+
class UnknownColumnError < StandardError
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
super("Unknown column '#{name}'")
|
10
|
+
@name = name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class InvalidValueError < StandardError
|
15
|
+
attr_reader :column, :value
|
16
|
+
|
17
|
+
def initialize(column, value)
|
18
|
+
super("Invalid value #{value.inspect} for column #{column.inspect}")
|
19
|
+
@column, @value = column, value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Class that contains data for particular table row.
|
24
|
+
# Should not be created explicitly (use Table#create to create records)
|
25
|
+
#
|
26
|
+
# == Accessing attributes
|
27
|
+
#
|
28
|
+
# You can read and assign values to row's columns using simple property syntax:
|
29
|
+
#
|
30
|
+
# user = users_table[0]
|
31
|
+
# user.name = 'Bob'
|
32
|
+
# user.birth_date = Date.new(1980, 2, 29)
|
33
|
+
# user.save
|
34
|
+
#
|
35
|
+
# puts user.name
|
36
|
+
# puts user.birth_date
|
37
|
+
#
|
38
|
+
class Record
|
39
|
+
attr_reader :table, :index
|
40
|
+
|
41
|
+
def initialize(table, attributes = {})
|
42
|
+
@table = table
|
43
|
+
@values_cached = {}
|
44
|
+
@values_changed = {}
|
45
|
+
|
46
|
+
attributes.each { |k, v| @values_changed[k.to_s.upcase] = v }
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def load(index, data)
|
52
|
+
@table = table
|
53
|
+
@index = index
|
54
|
+
@data = data.dup
|
55
|
+
end
|
56
|
+
|
57
|
+
public
|
58
|
+
|
59
|
+
# Returns true if record was never saved to database; otherwise return false.
|
60
|
+
def new_record?
|
61
|
+
@data.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Save record to database.
|
65
|
+
def save
|
66
|
+
record = self
|
67
|
+
@table.instance_eval { save(record) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Delete record from database.
|
71
|
+
def delete
|
72
|
+
@deleted = true
|
73
|
+
save
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns true if record was marked as deleted; otherwise return false.
|
77
|
+
def deleted?
|
78
|
+
@deleted ||= new_record? ? false : @data[0, 1] == '*'
|
79
|
+
end
|
80
|
+
|
81
|
+
# Clone record.
|
82
|
+
def clone
|
83
|
+
c = self.class.new(@table, @values_changed)
|
84
|
+
c.instance_variable_set("@values_cached", @values_cached)
|
85
|
+
c.instance_variable_set("@data", @data)
|
86
|
+
c
|
87
|
+
end
|
88
|
+
|
89
|
+
def method_missing(sym, *args)
|
90
|
+
name = sym.to_s
|
91
|
+
if /=$/ =~ name && args.size == 1
|
92
|
+
set_value(name[0..-2], args.first)
|
93
|
+
else
|
94
|
+
get_value(name)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def serialize
|
99
|
+
if new_record?
|
100
|
+
@data = deleted? ? '*' : ' '
|
101
|
+
@data << @table.columns.collect do |column|
|
102
|
+
column.pack(@values_changed[column.name])
|
103
|
+
end.join
|
104
|
+
else
|
105
|
+
@data[0, 1] = deleted? ? '*' : ' '
|
106
|
+
@values_changed.each do |k, v|
|
107
|
+
column = @table.column(k)
|
108
|
+
raise UnknownColumnError.new(k) unless column
|
109
|
+
begin
|
110
|
+
@data[column.offset, column.size] = column.pack(v)
|
111
|
+
rescue Object => e
|
112
|
+
raise InvalidValueError.new(column, v)
|
113
|
+
end
|
114
|
+
@values_cached[k] = v
|
115
|
+
end
|
116
|
+
end
|
117
|
+
@data
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
# Returns value of specified column
|
123
|
+
def get_value(name)
|
124
|
+
name = name.to_s.upcase.to_sym
|
125
|
+
return @values_changed[name] if @values_changed.has_key?(name)
|
126
|
+
return nil if new_record?
|
127
|
+
column = @table.column(name)
|
128
|
+
raise UnknownColumnError.new(name) unless column
|
129
|
+
@values_cached[name] ||= column.unpack(@data[column.offset, column.size])
|
130
|
+
end
|
131
|
+
|
132
|
+
# Sets value of specified column.
|
133
|
+
def set_value(name, value)
|
134
|
+
name = name.to_s.upcase.to_sym
|
135
|
+
raise UnknownColumnError.new(name) unless @table.column(name)
|
136
|
+
@values_changed[name] = value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
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 RBase::Table opened
|
8
|
+
#
|
9
|
+
# == Example
|
10
|
+
#
|
11
|
+
# users = RBase::Table.open('users')
|
12
|
+
# File.open('users.dump.rb', 'w') do |f|
|
13
|
+
# f.write RBase::SchemaDumper.dump(users)
|
14
|
+
# end
|
15
|
+
# users.close
|
16
|
+
#
|
17
|
+
def self.dump(table)
|
18
|
+
output = ''
|
19
|
+
output << "RBase.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,239 @@
|
|
1
|
+
module RBase
|
2
|
+
|
3
|
+
class Table
|
4
|
+
private_class_method :new
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
|
9
|
+
#
|
10
|
+
# Allowed options
|
11
|
+
# * :language - language character set used in database. Can be one of LANGUAGE_* constants
|
12
|
+
def self.create(name, schema, options = {})
|
13
|
+
date = Date.today
|
14
|
+
|
15
|
+
record_size = 1+schema.columns.inject(0) { |size, column| size + column.size }
|
16
|
+
|
17
|
+
data = ''
|
18
|
+
# data << [0xf5].pack('C') # version
|
19
|
+
data << [0x3].pack('C') # version
|
20
|
+
data << [date.year % 100, date.month, date.day].pack('CCC') # last modification date
|
21
|
+
data << [0].pack('L') # number of records
|
22
|
+
# data << [32+schema.columns.size*32+263+1].pack('v') # data size
|
23
|
+
data << [32+schema.columns.size*32+1].pack('v') # data size
|
24
|
+
data << [record_size].pack('v') # record size
|
25
|
+
data << [].pack('x2') # reserved
|
26
|
+
data << [].pack('x') # incomplete transaction
|
27
|
+
data << [].pack('x') # encyption flag
|
28
|
+
data << [].pack('x4') # reserved
|
29
|
+
data << [].pack('x8') # reserved
|
30
|
+
data << [0].pack('c') # mdx flag
|
31
|
+
data << [options[:language]].pack('C') # language driver
|
32
|
+
data << [].pack('x2') # reserved
|
33
|
+
|
34
|
+
offset = 1 # take into account 1 byte for deleted flag
|
35
|
+
data << schema.columns.collect do |column|
|
36
|
+
s = ''
|
37
|
+
s << [column.name.to_s[0..9]].pack('a11') # field name
|
38
|
+
s << [column.type].pack('a') # field type
|
39
|
+
s << [offset].pack('L') # field data offset
|
40
|
+
s << [column.size].pack('C') # field size
|
41
|
+
s << [column.decimal || 0].pack('C') # decimal count
|
42
|
+
s << [].pack('x2') # reserved
|
43
|
+
s << [].pack('x') # work area id
|
44
|
+
s << [].pack('x2') # reserved
|
45
|
+
s << [].pack('x') # flag for SET FIELDS
|
46
|
+
s << [].pack('x7') # reserved
|
47
|
+
s << [].pack('x') # index field flag
|
48
|
+
offset += column.size
|
49
|
+
s
|
50
|
+
end.join
|
51
|
+
|
52
|
+
data << [13].pack('C') # terminator
|
53
|
+
|
54
|
+
data << [26].pack('C') # end of file
|
55
|
+
|
56
|
+
File.open("#{name}.dbf", 'wb') do |f|
|
57
|
+
f.write data
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Open table with given name.
|
62
|
+
# Table name should be like file name without ".dbf" suffix.
|
63
|
+
def self.open(name, options = {})
|
64
|
+
table = new
|
65
|
+
table.instance_eval { open("#{name}.dbf", options) }
|
66
|
+
if block_given?
|
67
|
+
result = yield table
|
68
|
+
table.close
|
69
|
+
result
|
70
|
+
else
|
71
|
+
table
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Physically remove records that were marked as deleted from file.
|
76
|
+
def pack
|
77
|
+
packed_count = 0
|
78
|
+
count.times do |i|
|
79
|
+
@file.pos = @record_offset + @record_size*i
|
80
|
+
data = @file.read(@record_size)
|
81
|
+
unless data[0, 1]=='*'
|
82
|
+
if i!=packed_count
|
83
|
+
@file.pos = @record_offset + @record_size*packed_count
|
84
|
+
@file.write data
|
85
|
+
end
|
86
|
+
packed_count += 1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
file_end = @record_offset + @record_size*packed_count
|
91
|
+
@file.pos = file_end
|
92
|
+
@file.write "\x1a"
|
93
|
+
@file.truncate file_end+1
|
94
|
+
|
95
|
+
self.count = packed_count
|
96
|
+
update_header
|
97
|
+
end
|
98
|
+
|
99
|
+
def clear
|
100
|
+
file_end = @record_offset
|
101
|
+
@file.pos = file_end
|
102
|
+
@file.write "\x1a"
|
103
|
+
@file.truncate file_end+1
|
104
|
+
|
105
|
+
self.count = 0
|
106
|
+
update_header
|
107
|
+
end
|
108
|
+
|
109
|
+
def close
|
110
|
+
@file.close
|
111
|
+
end
|
112
|
+
|
113
|
+
attr_reader :name, :count, :columns, :last_modified_on, :language
|
114
|
+
|
115
|
+
# Return instance of RBase::Column for given column name
|
116
|
+
def column(name)
|
117
|
+
@name_to_columns[name]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns instance of MemoFile that is associated with table
|
121
|
+
def memo
|
122
|
+
return @memo_file
|
123
|
+
end
|
124
|
+
|
125
|
+
# Create new record, populate it with given attributes
|
126
|
+
def build(attributes = {})
|
127
|
+
Record.new(self, attributes)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Create new record, populate it with given attributes and save it
|
131
|
+
def create(attributes = {})
|
132
|
+
record = build(attributes)
|
133
|
+
record.save
|
134
|
+
record
|
135
|
+
end
|
136
|
+
|
137
|
+
# Load record stored in position 'index'
|
138
|
+
def load(index)
|
139
|
+
@file.pos = @record_offset + @record_size*index
|
140
|
+
data = @file.read(@record_size)
|
141
|
+
record = Record.new(self)
|
142
|
+
record.instance_eval { load(index, data) }
|
143
|
+
record
|
144
|
+
end
|
145
|
+
|
146
|
+
alias_method :[], :load
|
147
|
+
|
148
|
+
def []=(index, record)
|
149
|
+
record.instance_eval { @index = index }
|
150
|
+
save(record)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Iterate through all (even deleted) records
|
154
|
+
def each_with_deleted
|
155
|
+
return unless block_given?
|
156
|
+
|
157
|
+
count.times do |i|
|
158
|
+
yield load(i)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Iterate through all non-deleted records
|
163
|
+
def each
|
164
|
+
return unless block_given?
|
165
|
+
|
166
|
+
self.each_with_deleted do |record|
|
167
|
+
yield record unless record.deleted?
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def open(name, options = {})
|
174
|
+
@name = File.basename(name, '.dbf')
|
175
|
+
@file = File.open(name, "r+b")
|
176
|
+
header = @file.read(32)
|
177
|
+
|
178
|
+
year, month, day = *header.unpack('@1ccc')
|
179
|
+
year += 2000 if year >= 100
|
180
|
+
|
181
|
+
@last_modified_on = Date.new(year, month, day)
|
182
|
+
@count = header.unpack('@4V').first
|
183
|
+
@language = header.unpack('@29c').first
|
184
|
+
|
185
|
+
@record_offset = header.unpack('@8v').first
|
186
|
+
@record_size = header.unpack('@10v').first
|
187
|
+
|
188
|
+
@file.pos = 32
|
189
|
+
|
190
|
+
@columns = []
|
191
|
+
@name_to_columns = {}
|
192
|
+
column_options = {}
|
193
|
+
column_options[:encoding] = options[:encoding] if options[:encoding]
|
194
|
+
while true do
|
195
|
+
column_data = @file.read(32)
|
196
|
+
break if column_data[0, 1] == "\x0d"
|
197
|
+
name, type, offset, size, decimal = *column_data.unpack('@0a11aLCC')
|
198
|
+
name = name.strip
|
199
|
+
@columns << Columns::Column.column_for(type).new(name, options.merge(:offset => offset, :size => size, :decimal => decimal))
|
200
|
+
@name_to_columns[name.upcase.to_sym] = @columns.last
|
201
|
+
end
|
202
|
+
|
203
|
+
@columns.each { |column| column.attach_to(self) }
|
204
|
+
|
205
|
+
@memo_file = MemoFile::DummyMemoFile.new
|
206
|
+
end
|
207
|
+
|
208
|
+
def save(record)
|
209
|
+
if !record.index
|
210
|
+
@file.pos = @record_offset + @record_size*count
|
211
|
+
@file.write record.serialize
|
212
|
+
@file.write [26].pack('c')
|
213
|
+
record.instance_variable_set(:@index, count)
|
214
|
+
self.count = count + 1
|
215
|
+
else
|
216
|
+
throw "Index out of bound" if record.index>=count
|
217
|
+
@file.pos = @record_offset + @record_size*record.index
|
218
|
+
@file.write record.serialize
|
219
|
+
end
|
220
|
+
update_header
|
221
|
+
end
|
222
|
+
|
223
|
+
def count=(value)
|
224
|
+
@count = value
|
225
|
+
end
|
226
|
+
|
227
|
+
def last_modified_on=(value)
|
228
|
+
@last_modified_on = value
|
229
|
+
end
|
230
|
+
|
231
|
+
def update_header
|
232
|
+
@last_modified_on = Date.today
|
233
|
+
@file.pos = 1
|
234
|
+
@file.write([last_modified_on.year % 100, last_modified_on.month, last_modified_on.day].pack('ccc'))
|
235
|
+
@file.write([count].pack('V'))
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
data/lib/rbase.rb
ADDED
data/rbase.gemspec
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'rbase-ff'
|
3
|
+
s.version = '0.9'
|
4
|
+
s.summary = 'Library to create/read/write to XBase databases (*.DBF files)'
|
5
|
+
s.require_path = 'lib'
|
6
|
+
s.files = Dir.glob('**/*').delete_if { |item| item =~ /^\./ }
|
7
|
+
s.authors = 'Maxim Kulkin, Leonardo Augusto Pires, Tom Lahti'
|
8
|
+
s.email = 'maxim.kulkin@gmail.com, leonardo.pires@gmail.com, uidzip@gmail.com'
|
9
|
+
s.homepage = 'http://github.com/uidzip/rbase'
|
10
|
+
s.has_rdoc = true
|
11
|
+
|
12
|
+
s.required_ruby_version = '>= 1.9.3'
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rbase-ff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.9'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maxim Kulkin, Leonardo Augusto Pires, Tom Lahti
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: maxim.kulkin@gmail.com, leonardo.pires@gmail.com, uidzip@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- README.md
|
20
|
+
- lib/rbase.rb
|
21
|
+
- lib/rbase/builder.rb
|
22
|
+
- lib/rbase/columns.rb
|
23
|
+
- lib/rbase/encoder.rb
|
24
|
+
- lib/rbase/memo_file.rb
|
25
|
+
- lib/rbase/record.rb
|
26
|
+
- lib/rbase/schema.rb
|
27
|
+
- lib/rbase/schema_dumper.rb
|
28
|
+
- lib/rbase/table.rb
|
29
|
+
- rbase.gemspec
|
30
|
+
homepage: http://github.com/uidzip/rbase
|
31
|
+
licenses: []
|
32
|
+
metadata: {}
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.9.3
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 2.6.11
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: Library to create/read/write to XBase databases (*.DBF files)
|
53
|
+
test_files: []
|