forked from cfdrake/go-gdbm
/
gdbm.go
270 lines (233 loc) · 6.75 KB
/
gdbm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// Packge gdbm implements a wrapper around libgdbm, the GNU DataBase Manager
// library, for Go.
package gdbm
// #cgo CFLAGS: -std=gnu99
// #cgo LDFLAGS: -lgdbm
// #include <stdlib.h>
// #include <gdbm.h>
// #include <string.h>
// inline datum mk_datum(char * s) {
// datum d;
// d.dptr = s;
// d.dsize = strlen(s);
// return d;
// }
import "C"
import (
"errors"
"unsafe"
)
// GDBM database "connection" type.
type Database struct {
dbf C.GDBM_FILE
mode int
}
// GDBM database configuration type that can be used to specify how the
// database is created and treated. The `Mode` determines what the user of the
// `Database` object can do with it. The field can be a string, either "r" for
// Read-only, "w" for Read-Write, "c" for Read-Write and create if it doesn't
// exist, and "n" for Read-Write and always recreate database, even if it
// exists. TODO: write descriptions for other params... too lazy right now
type DatabaseCfg struct {
Mode string
BlockSize int
Permissions int
}
var (
// The error received when the end of the database is reached
NoError = errors.New("No error")
)
func lastError() error {
str := C.GoString(C.gdbm_strerror(C.gdbm_errno))
if str == "No error" {
return NoError
}
return errors.New(str)
}
// return the gdbm release build string
func Version() (version string) {
return C.GoString(C.gdbm_version)
}
/*
Simple function to open a database file with default parameters (block size
is default for the filesystem and file permissions are set to 0666).
mode is one of:
"r" - reader
"w" - writer
"c" - rw / create
"n" - new db
*/
func Open(filename string, mode string) (db *Database, err error) {
return OpenWithCfg(filename, DatabaseCfg{mode, 0, 0666})
}
// More complex database initialization function that takes in a `DatabaseCfg`
// struct to allow more fine-grained control over database settings.
func OpenWithCfg(filename string, cfg DatabaseCfg) (db *Database, err error) {
db = new(Database)
// Convert a human-readable mode string into a libgdbm-usable constant.
switch cfg.Mode {
case "r":
db.mode = C.GDBM_READER
case "w":
db.mode = C.GDBM_WRITER
case "c":
db.mode = C.GDBM_WRCREAT
case "n":
db.mode = C.GDBM_NEWDB
}
cs := C.CString(filename)
defer C.free(unsafe.Pointer(cs))
db.dbf = C.gdbm_open(cs, C.int(cfg.BlockSize), C.int(db.mode), C.int(cfg.Permissions), nil)
if db.dbf == nil {
err = lastError()
}
return db, err
}
// Closes a database's internal file pointer.
func (db *Database) Close() {
C.gdbm_close(db.dbf)
}
// Internal helper method to hide the two constants GDBM_INSERT and
// GDBM_REPLACE from the user.
func (db *Database) update(key string, value string, flag C.int) (err error) {
// Convert key and value into libgdbm's `datum` data structure. See the
// C definition at the top for the implementation of C.mk_datum(string).
kcs := C.CString(key)
vcs := C.CString(value)
k := C.mk_datum(kcs)
v := C.mk_datum(vcs)
defer C.free(unsafe.Pointer(kcs))
defer C.free(unsafe.Pointer(vcs))
retv := C.gdbm_store(db.dbf, k, v, flag)
if retv != 0 {
err = lastError()
}
return err
}
// Inserts a key-value pair into the database. If the database is opened
// in "r" mode, this will return an error. Also, if the key already exists in
// the database, and error will be returned.
func (db *Database) Insert(key string, value string) (err error) {
return db.update(key, value, C.GDBM_INSERT)
}
// Updates a key-value pair to use a new value, specified by the `value` string
// parameter. An error will be returned if the database is opened in "r" mode.
func (db *Database) Replace(key string, value string) (err error) {
return db.update(key, value, C.GDBM_REPLACE)
}
// Returns true or false, depending on whether the specified key exists in the
// database.
func (db *Database) Exists(key string) bool {
kcs := C.CString(key)
k := C.mk_datum(kcs)
defer C.free(unsafe.Pointer(kcs))
e := C.gdbm_exists(db.dbf, k)
if e == 1 {
return true
}
return false
}
// Returns the firstkey in this gdbm.Database.
// The traversal is ordered by gdbm‘s internal hash values, and won’t be sorted by the key values
// If there is not a key, an error will be returned in err.
func (db *Database) FirstKey() (value string, err error) {
vdatum := C.gdbm_firstkey(db.dbf)
if vdatum.dptr == nil {
return "", lastError()
}
value = C.GoStringN(vdatum.dptr, vdatum.dsize)
defer C.free(unsafe.Pointer(vdatum.dptr))
return value, nil
}
/*
Returns the nextkey after `key`. If there is not a next key, an
NoError error will be returned.
An Iteration might look like:
k, err := db.FirstKey()
if err != nil {
return err
}
for {
v, err := db.Fetch(k)
if err != nil {
return err
}
fmt.Println(v)
k, err = db.NextKey(k)
if err == gdbm.NoError {
break
} else if err != nil {
return err
}
}
*/
func (db *Database) NextKey(key string) (value string, err error) {
kcs := C.CString(key)
k := C.mk_datum(kcs)
defer C.free(unsafe.Pointer(kcs))
vdatum := C.gdbm_nextkey(db.dbf, k)
if vdatum.dptr == nil {
return "", lastError()
}
value = C.GoStringN(vdatum.dptr, vdatum.dsize)
defer C.free(unsafe.Pointer(vdatum.dptr))
return value, nil
}
/*
Convinience to get all the keys from this database
*/
func (db *Database) Keys() ([]string, error) {
keys := []string{}
k, err := db.FirstKey()
if err != nil {
return keys, err
}
keys = append(keys, k)
for {
k, err = db.NextKey(k)
if err == NoError {
break
} else if err != nil {
return keys, err
}
keys = append(keys, k)
}
return keys, nil
}
// Fetches the value of the given key. If the key is not in the database, an
// error will be returned in err. Otherwise, value will be the value string
// that is keyed by `key`.
func (db *Database) Fetch(key string) (value string, err error) {
kcs := C.CString(key)
k := C.mk_datum(kcs)
defer C.free(unsafe.Pointer(kcs))
vdatum := C.gdbm_fetch(db.dbf, k)
if vdatum.dptr == nil {
return "", lastError()
}
value = C.GoStringN(vdatum.dptr, vdatum.dsize)
defer C.free(unsafe.Pointer(vdatum.dptr))
return value, nil
}
// Removes a key-value pair from the database. If the database is opened in "r"
// mode, an error is returned
func (db *Database) Delete(key string) (err error) {
kcs := C.CString(key)
k := C.mk_datum(kcs)
defer C.free(unsafe.Pointer(kcs))
retv := C.gdbm_delete(db.dbf, k)
if retv == -1 && db.mode == C.GDBM_READER {
err = lastError()
}
return err
}
// Reorganizes the database for more efficient use of disk space. This method
// can be used if Delete(k) is called many times.
func (db *Database) Reorganize() {
C.gdbm_reorganize(db.dbf)
}
// Synchronizes all pending database changes to the disk. TODO: note this is
// only needed in FAST mode, and FAST mode needs implemented!
func (db *Database) Sync() {
C.gdbm_sync(db.dbf)
}