rbase-ff 0.9
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.
- 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: []
|