Add golang bindings
authorMarvin Killing <marvinkilling@gmail.com>
Mon, 8 Jul 2013 22:16:43 +0000 (00:16 +0200)
committerMarvin Killing <marvinkilling@gmail.com>
Sat, 13 Jul 2013 12:08:32 +0000 (14:08 +0200)
goejdb/README [new file with mode: 0644]
goejdb/src/github.com/mkilling/ejdb/goejdb/ejcoll.go [new file with mode: 0644]
goejdb/src/github.com/mkilling/ejdb/goejdb/ejdb.go [new file with mode: 0644]
goejdb/src/github.com/mkilling/ejdb/goejdb/ejdb_test.go [new file with mode: 0644]
goejdb/src/github.com/mkilling/ejdb/goejdb/ejquery.go [new file with mode: 0644]
goejdb/src/github.com/mkilling/ejdb/goejdb/util.go [new file with mode: 0644]

diff --git a/goejdb/README b/goejdb/README
new file mode 100644 (file)
index 0000000..838ac9b
--- /dev/null
@@ -0,0 +1,204 @@
+EJDB Go Library
+==================================
+
+One snippet intro
+-----------------------------------
+
+~~~~~~
+package ejdbtutorial
+
+import (
+    "fmt"
+    "github.com/mkilling/ejdb/goejdb"
+    "labix.org/v2/mgo/bson"
+    "os"
+)
+
+func main() {
+    jb, err := goejdb.Open("addressbook", JBOWRITER | JBOCREAT | JBOTRUNC)
+    if err != nil {
+        os.Exit(1)
+    }
+    //Get or create collection 'contacts'
+    coll, _ := jb.CreateColl("contacts", nil)
+
+    //Insert one record:
+    //JSON: {'name' : 'Bruce', 'phone' : '333-222-333', 'age' : 58}
+    rec := map[string]interface{} {"name" : "Bruce", "phone" : "333-222-333", "age" : 58}
+    bsrec, _ := bson.Marshal(rec)
+    coll.SaveBson(bsrec)
+    fmt.Printf("\nSaved Bruce")
+
+    //Now execute query
+    res, _ := coll.Find(`{"name" : {"$begin" : "Bru"}}`) //Name starts with 'Bru' string
+    fmt.Printf("\n\nRecords found: %d\n", len(res))
+
+    //Now print the result set records
+    for _, bs := range res {
+        var m map[string]interface{}
+        bson.Unmarshal(bs, &m)
+        fmt.Println(m)
+    }
+
+    //Close database
+    jb.Close()
+}
+~~~~~~
+
+You can save this code in `ejdbtutorial.go` And build:
+
+
+```sh
+go build ejdbtutorial.go
+./ejdbtutorial
+```
+
+Building & Installation
+--------------------------------
+
+[Installing on Debian/Ubuntu](https://github.com/Softmotions/ejdb/wiki/Debian-Ubuntu-installation)
+
+Manual installation
+-------------------------------
+
+### Prerequisites
+**System libraries:**
+
+* Google Go
+* labix.org/v2/mgo/bson (go get labix.org/v2/mgo/bson)
+* tcejdb (see tcejdb/ in this repository)
+
+### Build and install
+
+~~~~~~
+   go get github.com/mkilling/ejdb/goejdb
+~~~~~~
+
+Queries
+---------------------------------
+
+~~~~~~
+/**
+ * Create query object.
+ * Sucessfully created queries must be destroyed with ejdbquerydel().
+ *
+ * EJDB queries inspired by MongoDB (mongodb.org) and follows same philosophy.
+ *
+ *  - Supported queries:
+ *      - Simple matching of String OR Number OR Array value:
+ *          -   {'fpath' : 'val', ...}
+ *      - $not Negate operation.
+ *          -   {'fpath' : {'$not' : val}} //Field not equal to val
+ *          -   {'fpath' : {'$not' : {'$begin' : prefix}}} //Field not begins with val
+ *      - $begin String starts with prefix
+ *          -   {'fpath' : {'$begin' : prefix}}
+ *      - $gt, $gte (>, >=) and $lt, $lte for number types:
+ *          -   {'fpath' : {'$gt' : number}, ...}
+ *      - $bt Between for number types:
+ *          -   {'fpath' : {'$bt' : [num1, num2]}}
+ *      - $in String OR Number OR Array val matches to value in specified array:
+ *          -   {'fpath' : {'$in' : [val1, val2, val3]}}
+ *      - $nin - Not IN
+ *      - $strand String tokens OR String array val matches all tokens in specified array:
+ *          -   {'fpath' : {'$strand' : [val1, val2, val3]}}
+ *      - $stror String tokens OR String array val matches any token in specified array:
+ *          -   {'fpath' : {'$stror' : [val1, val2, val3]}}
+ *      - $exists Field existence matching:
+ *          -   {'fpath' : {'$exists' : true|false}}
+ *      - $icase Case insensitive string matching:
+ *          -    {'fpath' : {'$icase' : 'val1'}} //icase matching
+ *          Ignore case matching with '$in' operation:
+ *          -    {'name' : {'$icase' : {'$in' : ['tHéâtre - театр', 'heLLo WorlD']}}}
+ *          For case insensitive matching you can create special index of type: `JBIDXISTR`
+ *     - $elemMatch The $elemMatch operator matches more than one component within an array element.
+ *          -  { array: { $elemMatch: { value1 : 1, value2 : { $gt: 1 } } } }
+ *          Restriction: only one $elemMatch allowed in context of one array field.
+ *
+ *  - Queries can be used to update records:
+ *       $set Field set operation.
+ *           - {.., '$set' : {'field1' : val1, 'fieldN' : valN}}
+ *       $upsert Atomic upsert. If matching records are found it will be '$set' operation,
+ *              otherwise new record will be inserted with fields specified by argment object.
+ *           - {.., '$upsert' : {'field1' : val1, 'fieldN' : valN}}
+ *       $inc Increment operation. Only number types are supported.
+ *           - {.., '$inc' : {'field1' : number, ...,  'field1' : number}
+ *       $dropall In-place record removal operation.
+ *           - {.., '$dropall' : true}
+ *       $addToSet Atomically adds value to the array only if its not in the array already.
+ *                    If containing array is missing it will be created.
+ *           - {.., '$addToSet' : {'fpath' : val1, 'fpathN' : valN, ...}}
+ *       $addToSetAll Batch version if $addToSet
+ *           - {.., '$addToSetAll' : {'fpath' : [array of values to add], ...}}
+ *       $pull Atomically removes all occurrences of value from field, if field is an array.
+ *           - {.., '$pull' : {'fpath' : val1, 'fpathN' : valN, ...}}
+ *       $pullAll Batch version of $pull
+ *           - {.., '$pullAll' : {'fpath' : [array of values to remove], ...}}
+ *
+ *  NOTE: Negate operations: $not and $nin not using indexes
+ *  so they can be slow in comparison to other matching operations.
+ *
+ *  NOTE: Only one index can be used in search query operation.
+ *
+ *  QUERY HINTS (specified by `hints` argument):
+ *      - $max Maximum number in the result set
+ *      - $skip Number of skipped results in the result set
+ *      - $orderby Sorting order of query fields.
+ *      - $fields Set subset of fetched fields
+            If a field presented in $orderby clause it will be forced to include in resulting records.
+ *          Example:
+ *          hints:    {
+ *                      "$orderby" : { //ORDER BY field1 ASC, field2 DESC
+ *                          "field1" : 1,
+ *                          "field2" : -1
+ *                      },
+ *                      "$fields" : { //SELECT ONLY {_id, field1, field2}
+ *                          "field1" : 1,
+ *                          "field2" : 1
+ *                      }
+ *                    }
+ *
+ * Many query examples can be found in `testejdb/t2.c` test case.
+ *
+ * @param EJDB database handle.
+ * @param qobj Main BSON query object.
+ * @param orqobjs Array of additional OR query objects (joined with OR predicate).
+ * @param orqobjsnum Number of OR query objects.
+ * @param hints BSON object with query hints.
+ * @return On success return query handle. On error returns NULL.
+ */
+EJDB_EXPORT EJQ* ejdbcreatequery(EJDB *jb, bson *qobj, bson *orqobjs, int orqobjsnum, bson *hints);
+~~~~~~
+
+Basic EJDB architecture
+------------------------------------
+**EJDB database files structure**
+
+~~~~~~
+.
+├── <dbname>
+├── <dbname>_<collection1>
+├── ...
+├── <dbname>_<collectionN>
+└── <dbname>_<collectionN>_<fieldpath>.<index ext>
+~~~~~~
+
+Where
+
+* ```<dbname>``` - name of database. It is metadata DB.
+* ```<collectionN>``` - name of collection. Collection database.
+* ```<fieldpath>``` - JSON field path used in index
+* ```<index ext>``` - Collection index extension:
+    * ```.lex``` String index
+    * ```.dec``` Number index
+    * ```.tok``` Array index
+
+Limitations
+------------------------------------
+* One ejdb database can handle up to 1024 collections.
+* Indexes for objects in nested arrays currently not supported (#37)
+
+Related software
+------------------------------------
+[Connect session store backed by EJDB database](https://github.com/Softmotions/connect-session-ejdb)
+
+
diff --git a/goejdb/src/github.com/mkilling/ejdb/goejdb/ejcoll.go b/goejdb/src/github.com/mkilling/ejdb/goejdb/ejcoll.go
new file mode 100644 (file)
index 0000000..af0a0bd
--- /dev/null
@@ -0,0 +1,174 @@
+package goejdb
+
+// #cgo LDFLAGS: -ltcejdb
+// #include "../../../../../../tcejdb/ejdb.h"
+import "C"
+
+import "unsafe"
+
+const (
+       JBIDXDROP    = C.JBIDXDROP
+       JBIDXDROPALL = C.JBIDXDROPALL
+       JBIDXOP      = C.JBIDXOP
+       JBIDXREBLD   = C.JBIDXREBLD
+       JBIDXNUM     = C.JBIDXNUM
+       JBIDXSTR     = C.JBIDXSTR
+       JBIDXARR     = C.JBIDXARR
+       JBIDXISTR    = C.JBIDXISTR
+)
+
+type EjColl struct {
+       ptr  *[0]byte
+       ejdb *Ejdb
+}
+
+type EjCollOpts struct {
+       large         bool
+       compressed    bool
+       records       int
+       cachedrecords int
+}
+
+func (coll *EjColl) save_c_bson(c_bson *C.bson) (string, *EjdbError) {
+       var c_oid C.bson_oid_t
+       C.ejdbsavebson(coll.ptr, c_bson, &c_oid)
+       return bson_oid_to_string(&c_oid), coll.ejdb.check_error()
+}
+
+func (coll *EjColl) SaveBson(bsdata []byte) (string, *EjdbError) {
+       c_bson := bson_from_byte_slice(bsdata)
+       defer C.bson_destroy(c_bson)
+       return coll.save_c_bson(c_bson)
+}
+
+func (coll *EjColl) SaveJson(j string) (string, *EjdbError) {
+       c_bson := bson_from_json(j)
+       defer C.bson_destroy(c_bson)
+       return coll.save_c_bson(c_bson)
+}
+
+// EJDB_EXPORT bool ejdbrmbson(EJCOLL *coll, bson_oid_t *oid);
+func (coll *EjColl) RmBson(oid string) bool {
+       c_oid := bson_oid_from_string(&oid)
+       ret := C.ejdbrmbson(coll.ptr, c_oid)
+       coll.ejdb.check_error()
+       return bool(ret)
+}
+
+// EJDB_EXPORT bson* ejdbloadbson(EJCOLL *coll, const bson_oid_t *oid);
+func (coll *EjColl) LoadBson(oid string) []byte {
+       c_oid := bson_oid_from_string(&oid)
+       bson := C.ejdbloadbson(coll.ptr, c_oid)
+       defer C.bson_del(bson)
+       coll.ejdb.check_error()
+
+       return bson_to_byte_slice(bson)
+}
+
+// EJDB_EXPORT EJQRESULT ejdbqryexecute(EJCOLL *jcoll, const EJQ *q, uint32_t *count, int qflags, TCXSTR *log);
+func (coll *EjColl) Find(query string, queries ...string) ([][]byte, *EjdbError) {
+       q, err := coll.ejdb.CreateQuery(query, queries...)
+       defer q.Del()
+       if err != nil {
+               return nil, err
+       } else {
+               return q.Execute(coll)
+       }
+}
+
+func (coll *EjColl) FindOne(query string, queries ...string) (*[]byte, *EjdbError) {
+       q, err := coll.ejdb.CreateQuery(query, queries...)
+       defer q.Del()
+       if err != nil {
+               return nil, err
+       } else {
+               return q.ExecuteOne(coll)
+       }
+}
+
+func (coll *EjColl) Count(query string, queries ...string) (int, *EjdbError) {
+       q, err := coll.ejdb.CreateQuery(query, queries...)
+       if err != nil {
+               return 0, err
+       }
+       defer q.Del()
+       return q.Count(coll)
+}
+
+// EJDB_EXPORT uint32_t ejdbupdate(EJCOLL *jcoll, bson *qobj, bson *orqobjs, int orqobjsnum, bson *hints, TCXSTR *log);
+func (coll *EjColl) Update(query string, queries ...string) (int, *EjdbError) {
+       query_bson := bson_from_json(query)
+       defer C.bson_destroy(query_bson)
+
+       orqueries := C.malloc((C.size_t)(unsafe.Sizeof(new(C.bson))) * C.size_t(len(queries)))
+       defer C.free(orqueries)
+       ptr_orqueries := (*[maxslice]*C.bson)(orqueries)
+       for i, q := range queries {
+               (*ptr_orqueries)[i] = bson_from_json(q)
+               defer C.bson_destroy((*ptr_orqueries)[i])
+       }
+
+       count := C.ejdbupdate(coll.ptr, query_bson, (*C.bson)(orqueries), C.int(len(queries)), nil, nil)
+       return int(count), coll.ejdb.check_error()
+}
+
+// EJDB_EXPORT bool ejdbsetindex(EJCOLL *coll, const char *ipath, int flags);
+func (coll *EjColl) SetIndex(ipath string, flags int) *EjdbError {
+       c_ipath := C.CString(ipath)
+       defer C.free(unsafe.Pointer(c_ipath))
+       res := C.ejdbsetindex(coll.ptr, c_ipath, C.int(flags))
+       if res {
+               return nil
+       } else {
+               return coll.ejdb.check_error()
+       }
+}
+
+// EJDB_EXPORT bool ejdbtranbegin(EJCOLL *coll);
+func (coll *EjColl) BeginTransaction() *EjdbError {
+       res := C.ejdbtranbegin(coll.ptr)
+       if res {
+               return nil
+       } else {
+               return coll.ejdb.check_error()
+       }
+}
+
+//EJDB_EXPORT bool ejdbtrancommit(EJCOLL *coll);
+func (coll *EjColl) CommitTransaction() *EjdbError {
+       res := C.ejdbtrancommit(coll.ptr)
+       if res {
+               return nil
+       } else {
+               return coll.ejdb.check_error()
+       }
+}
+
+// EJDB_EXPORT bool ejdbtranabort(EJCOLL *coll);
+func (coll *EjColl) AbortTransaction() *EjdbError {
+       res := C.ejdbtranabort(coll.ptr)
+       if res {
+               return nil
+       } else {
+               return coll.ejdb.check_error()
+       }
+}
+
+// EJDB_EXPORT bool ejdbtranstatus(EJCOLL *jcoll, bool *txactive);
+func (coll *EjColl) IsTransactionActive() bool {
+       var ret C.bool
+       C.ejdbtranstatus(coll.ptr, &ret)
+       return bool(ret)
+}
+
+// EJDB_EXPORT void ejdbqresultdispose(EJQRESULT qr);
+
+// EJDB_EXPORT bool ejdbsyncoll(EJCOLL *jcoll);
+func (coll *EjColl) Sync() (bool, *EjdbError) {
+       ret := C.ejdbsyncoll(coll.ptr)
+       if ret {
+               return bool(ret), nil
+       } else {
+               return bool(ret), coll.ejdb.check_error()
+       }
+}
diff --git a/goejdb/src/github.com/mkilling/ejdb/goejdb/ejdb.go b/goejdb/src/github.com/mkilling/ejdb/goejdb/ejdb.go
new file mode 100644 (file)
index 0000000..74b4310
--- /dev/null
@@ -0,0 +1,174 @@
+package goejdb
+
+// #cgo LDFLAGS: -ltcejdb
+// #include "../../../../../../tcejdb/ejdb.h"
+import "C"
+
+import (
+       "errors"
+       "fmt"
+       "unsafe"
+)
+
+const (
+       JBOREADER = C.JBOREADER
+       JBOWRITER = C.JBOWRITER
+       JBOCREAT  = C.JBOCREAT
+       JBOTRUNC  = C.JBOTRUNC
+       JBONOLCK  = C.JBONOLCK
+       JBOLCKNB  = C.JBOLCKNB
+       JBOTSYNC  = C.JBOTSYNC
+)
+
+const maxslice = 1<<31 - 1
+
+type Ejdb struct {
+       ptr *[0]byte
+}
+
+type EjdbError struct {
+       ErrorCode int
+       error
+}
+
+func new_ejdb() *Ejdb {
+       ejdb := new(Ejdb)
+       ejdb.ptr = C.ejdbnew()
+       if ejdb.ptr == nil {
+               return nil
+       } else {
+               return ejdb
+       }
+}
+
+func Version() string {
+       cs := C.ejdbversion()
+       return C.GoString(cs)
+}
+
+func IsValidOidStr(oid string) bool {
+       c_oid := C.CString(oid)
+       res := C.ejdbisvalidoidstr(c_oid)
+       C.free(unsafe.Pointer(c_oid))
+
+       return bool(res)
+}
+
+func Open(path string, options int) (*Ejdb, *EjdbError) {
+       ejdb := new_ejdb()
+       if ejdb != nil {
+               c_path := C.CString(path)
+               defer C.free(unsafe.Pointer(c_path))
+               C.ejdbopen(ejdb.ptr, c_path, C.int(options))
+       }
+
+       return ejdb, ejdb.check_error()
+}
+
+func (ejdb *Ejdb) check_error() *EjdbError {
+       ecode := C.ejdbecode(ejdb.ptr)
+       if ecode == 0 {
+               return nil
+       }
+       c_msg := C.ejdberrmsg(ecode)
+       msg := C.GoString(c_msg)
+       return &EjdbError{int(ecode), errors.New(fmt.Sprintf("EJDB error: %v", msg))}
+}
+
+func (ejdb *Ejdb) IsOpen() bool {
+       ret := C.ejdbisopen(ejdb.ptr)
+       return bool(ret)
+}
+
+func (ejdb *Ejdb) Del() {
+       C.ejdbdel(ejdb.ptr)
+}
+
+func (ejdb *Ejdb) Close() *EjdbError {
+       C.ejdbclose(ejdb.ptr)
+       return ejdb.check_error()
+}
+
+func (ejdb *Ejdb) GetColl(colname string) (*EjColl, *EjdbError) {
+       c_colname := C.CString(colname)
+       defer C.free(unsafe.Pointer(c_colname))
+
+       ejcoll := new(EjColl)
+       ejcoll.ejdb = ejdb
+       ejcoll.ptr = C.ejdbgetcoll(ejdb.ptr, c_colname)
+
+       return ejcoll, ejdb.check_error()
+}
+
+func (ejdb *Ejdb) GetColls() ([]*EjColl, *EjdbError) {
+       ret := make([]*EjColl, 0)
+       lst := C.ejdbgetcolls(ejdb.ptr)
+       if lst == nil {
+               return ret, ejdb.check_error()
+       }
+
+       for i := int(lst.start); i < int(lst.start)+int(lst.num); i++ {
+               ptr := uintptr(unsafe.Pointer(lst.array)) + unsafe.Sizeof(C.TCLISTDATUM{})*uintptr(i)
+               datum := (*C.TCLISTDATUM)(unsafe.Pointer(ptr))
+               datum_ptr := unsafe.Pointer(datum.ptr)
+               ret = append(ret, &EjColl{(*[0]byte)(datum_ptr), ejdb})
+       }
+       return ret, nil
+}
+
+func (ejdb *Ejdb) CreateColl(colname string, opts *EjCollOpts) (*EjColl, *EjdbError) {
+       c_colname := C.CString(colname)
+       defer C.free(unsafe.Pointer(c_colname))
+
+       ret := new(EjColl)
+       ret.ejdb = ejdb
+
+       if opts != nil {
+               var c_opts C.EJCOLLOPTS
+               c_opts.large = C._Bool(opts.large)
+               c_opts.compressed = C._Bool(opts.large)
+               c_opts.records = C.int64_t(opts.records)
+               c_opts.cachedrecords = C.int(opts.cachedrecords)
+               ret.ptr = C.ejdbcreatecoll(ejdb.ptr, c_colname, &c_opts)
+       } else {
+               ret.ptr = C.ejdbcreatecoll(ejdb.ptr, c_colname, nil)
+       }
+
+       if ret.ptr != nil {
+               return ret, nil
+       } else {
+               return nil, ejdb.check_error()
+       }
+}
+
+func (ejdb *Ejdb) RmColl(colname string, unlinkfile bool) (bool, *EjdbError) {
+       c_colname := C.CString(colname)
+       defer C.free(unsafe.Pointer(c_colname))
+       res := C.ejdbrmcoll(ejdb.ptr, c_colname, C._Bool(unlinkfile))
+       if res {
+               return bool(res), nil
+       } else {
+               return bool(res), ejdb.check_error()
+       }
+}
+
+// EJDB_EXPORT bool ejdbsyncdb(EJDB *jb);
+func (ejdb *Ejdb) Sync() (bool, *EjdbError) {
+       ret := C.ejdbsyncdb(ejdb.ptr)
+       if ret {
+               return bool(ret), nil
+       } else {
+               return bool(ret), ejdb.check_error()
+       }
+}
+
+// EJDB_EXPORT bson* ejdbmeta(EJDB *jb);
+func (ejdb *Ejdb) Meta() ([]byte, *EjdbError) {
+       bson := C.ejdbmeta(ejdb.ptr)
+       err := ejdb.check_error()
+       if err != nil {
+               return make([]byte, 0), err
+       }
+       defer C.bson_del(bson)
+       return bson_to_byte_slice(bson), nil
+}
diff --git a/goejdb/src/github.com/mkilling/ejdb/goejdb/ejdb_test.go b/goejdb/src/github.com/mkilling/ejdb/goejdb/ejdb_test.go
new file mode 100644 (file)
index 0000000..f0544be
--- /dev/null
@@ -0,0 +1,421 @@
+package goejdb
+
+import (
+       "fmt"
+       "labix.org/v2/mgo/bson"
+       "sync"
+       "testing"
+       "os"
+)
+
+type TestType struct {
+       I int
+       S string
+       M map[string]string
+}
+
+func make_test_type() TestType {
+       return TestType{I: 5, S: "mystring", M: map[string]string{"1": "one", "2": "two"}}
+}
+
+var i int = 0
+var iMu sync.Mutex
+
+func open() *Ejdb {
+       iMu.Lock()
+       i++
+       ejdb, _ := Open(fmt.Sprintf("/tmp/my%d.ejdb", i), JBOWRITER|JBOCREAT|JBOTRUNC)
+       iMu.Unlock()
+       return ejdb
+}
+
+func TestVersion(t *testing.T) {
+       Version()
+}
+
+func TestIsValidOidStr(t *testing.T) {
+       if !IsValidOidStr("4fc62a0f4c114f273c000001") {
+               t.Errorf("4fc62a0f4c114f273c000001 not recognized as a valid oid str though it should be")
+       }
+
+       if IsValidOidStr("invalidoid") {
+               t.Errorf("invalidoid recognized as a valid oid str though it should not be")
+       }
+}
+
+func TestOpen(t *testing.T) {
+       ejdb := open()
+       if ejdb == nil || !ejdb.IsOpen() {
+               t.Errorf("Opening EJDB failed")
+       }
+}
+
+func TestDel(t *testing.T) {
+       ejdb := open()
+       ejdb.Del()
+}
+
+func TestClose(t *testing.T) {
+       ejdb := open()
+       ejdb.Close()
+       if ejdb.IsOpen() {
+               t.Errorf("Closing EJDB failed")
+       }
+}
+
+func TestCreateColl(t *testing.T) {
+       ejdb := open()
+       coll, err := ejdb.CreateColl("MyNewColl", nil)
+       if err != nil {
+               t.Errorf("CreateColl() failed with %v", err)
+       }
+       if coll == nil {
+               t.Errorf("CreateColl() returned nil")
+       }
+}
+
+func TestCreateCollWithOptions(t *testing.T) {
+       ejdb := open()
+       coll, err := ejdb.CreateColl("MyNewColl", &EjCollOpts{large: true, compressed: true, records: 1280000, cachedrecords: 0})
+       if err != nil {
+               t.Errorf("CreateColl() failed with %v", err)
+       }
+       if coll == nil {
+               t.Errorf("CreateColl() returned nil")
+       }
+}
+
+func TestGetColl(t *testing.T) {
+       ejdb := open()
+       ejdb.CreateColl("MyNewColl", nil)
+       coll, err := ejdb.GetColl("MyNewColl")
+       if err != nil {
+               t.Errorf("GetColl() failed with %v", err)
+       }
+       if coll == nil {
+               t.Errorf("GetColl() returned nil")
+       }
+}
+
+func TestGetColls(t *testing.T) {
+       ejdb := open()
+       ejdb.CreateColl("MyNewColl", nil)
+       ejdb.CreateColl("MyNewColl2", nil)
+
+       colls, err := ejdb.GetColls()
+       if err != nil {
+               t.Errorf("GetColls() failed with %v", err)
+       }
+       if len(colls) != 2 {
+               t.Errorf("expected GetColls() to return 2 collections, got %d", len(colls))
+       }
+}
+
+func TestRmColl(t *testing.T) {
+       ejdb := open()
+       ejdb.CreateColl("MyNewColl", nil)
+       ret, err := ejdb.RmColl("MyNewColl", true)
+       if err != nil {
+               t.Errorf("RmColl() failed with %v", err)
+       }
+       if ret == false {
+               t.Errorf("RmColl() returned false though collection did exist and it should have returned true")
+       }
+}
+
+func TestSaveBson(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       oid, _ := coll.SaveBson(bytes)
+       if oid == "" {
+               t.Errorf("SaveBson returned empty string instead of valid BSON OID")
+       }
+}
+
+func TestLoadBson(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       oid, _ := coll.SaveBson(bytes)
+
+       loaded := coll.LoadBson(oid)
+       var out TestType
+       bson.Unmarshal(loaded, &out)
+
+       if out.I != 5 {
+               t.Errorf("Unmarshalling int failed: expected 5 but was %v", out.I);
+       }
+       if out.S != "mystring" {
+               t.Errorf("Unmarshalling string failed: expected \"mystring\" but was %v", out.S);
+       }
+       if out.M["2"] != "two" {
+               t.Errorf("Unmarshalling map[string]string failed: expected entry [\"2\"] to be \"two\" but was %v", out.M["2"]);
+       }
+}
+
+func TestRmBson(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       oid, _ := coll.SaveBson(bytes)
+       coll.RmBson(oid)
+}
+
+func TestSyncColl(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       coll.SaveBson(bytes)
+       ret, err := coll.Sync()
+       if err != nil {
+               t.Errorf("Collection.Sync() failed with %v", err)
+       }
+       if ret == false {
+               t.Errorf("Collection.Sync() should have returned true but returned false")
+       }
+}
+
+func TestSyncEjdb(t *testing.T) {
+       ejdb := open()
+       ejdb.CreateColl("MyNewColl", nil)
+       ret, err := ejdb.Sync()
+       if err != nil {
+               t.Errorf("Ejdb.Sync() failed with %v", err)
+       }
+       if ret == false {
+               t.Errorf("Ejdb.Sync() should have returned true but returned false")
+       }
+}
+
+func TestEjdbMeta(t *testing.T) {
+       ejdb := open()
+       var m map[string]interface{}
+       bs, _ := ejdb.Meta()
+       bson.Unmarshal(bs, &m)
+       if m["file"] == nil {
+               t.Errorf("Metadata seems to be invalid, does not have an entry for [\"file\"]")
+       }
+}
+
+func TestFind(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       coll.SaveBson(bytes)
+
+       res1, err := coll.Find(`{"s" : "mystring"}`)
+       if err != nil {
+               t.Errorf("Find() failed with %v", err)
+       }
+       if len(res1) != 1 {
+               fmt.Println(res1)
+               t.Errorf("Find() did not find the right amount of entries. Expected 1 but got %v", len(res1))
+       }
+}
+
+func TestFindShouldReturnEmptySliceOnNoResults(t *testing.T) {
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+
+       res1, err := coll.Find(`{"s" : "mystring"}`)
+       if err != nil {
+               t.Errorf("Find() failed with %v", err)
+       }
+       if len(res1) != 0 {
+               fmt.Println(res1)
+               t.Errorf("Find() did not find the right amount of entries. Expected 0 but got %v", len(res1))
+       }
+}
+
+func TestCount(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       coll.SaveBson(bytes)
+       coll.SaveBson(bytes)
+
+       ct, err := coll.Count(`{"s" : "mystring"}`)
+       if err != nil {
+               t.Errorf("Count() failed with %v", err)
+       }
+       if ct != 2 {
+               t.Errorf("Count() did not return the right result. Expected 2 but got %v", ct)
+       }
+}
+
+func TestFindOneShouldReturnResult(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       coll.SaveBson(bytes)
+       coll.SaveBson(bytes)
+
+       ret, err := coll.FindOne(`{"s" : "mystring"}`)
+       if err != nil {
+               t.Errorf("FindOne() failed with %v", err)
+       }
+       if ret == nil {
+               t.Fatalf("FindOne() failed. Returned nil though there should be a result.")
+       }
+       var tt TestType
+       bson.Unmarshal(*ret, &tt)
+       if tt.S != "mystring" {
+               t.Errorf("FindOne() failed. Expected result to have entry [\"s\"] set to \"mystring\" but was \"%s\".", tt.S)
+       }
+}
+
+func TestFindOneShouldReturnNilOnNoResult(t *testing.T) {
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+
+       ret, err := coll.FindOne(`{"s" : "mystring"}`)
+       if err != nil {
+               t.Errorf("FindOne() failed with %v", err)
+       }
+       if ret != nil {
+               t.Fatalf("FindOne() failed. Returned non-nil result though it should have found nothing and returned nil.")
+       }
+}
+
+func TestUpdate(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       coll.SaveBson(bytes)
+       coll.SaveBson(bytes)
+
+       ct, err := coll.Update(`{"$inc": {"i": 5}}`)
+       if err != nil {
+               t.Errorf("Update() failed with %v", err)
+       }
+       if ct != 2 {
+               t.Errorf("Update() returned the wrong count. Expected 2 but got %d", ct)
+       }
+
+       // check the values after updating
+       res, _ := coll.Find(`{}`)
+       for _, r := range res {
+               var tt TestType
+               bson.Unmarshal(r, &tt)
+               if tt.I != 10 {
+                       t.Errorf("Update() failed. Expected all \"I\" entries to have been updated to 10 but this one was %v", tt.I)
+               }
+       }
+}
+
+func TestSetIndex(t *testing.T) {
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+       for i := 0; i < 10; i++ {
+               coll.SaveJson(fmt.Sprintf(`{"i": %d, "s": "number%d"`, i))
+       }
+
+       err1 := coll.SetIndex("i", JBIDXNUM)
+       if err1 != nil {
+               t.Errorf("SetIndex() with JBIDXNUM failed with %v", err1)
+       }
+       err2 := coll.SetIndex("s", JBIDXSTR)
+       if err2 != nil {
+               t.Errorf("SetIndex() with JBIDXSTR failed with %v", err2)
+       }
+}
+
+func TestTransactions(t *testing.T) {
+       bytes, _ := bson.Marshal(make_test_type())
+
+       ejdb := open()
+       coll, _ := ejdb.CreateColl("MyNewColl", nil)
+
+       coll.BeginTransaction()
+       coll.SaveBson(bytes)
+       coll.SaveBson(bytes)
+       coll.AbortTransaction()
+
+       ct, _ := coll.Count(`{}`)
+       if ct != 0 {
+               t.Errorf("transaction not aborted successfully")
+       }
+
+       if coll.IsTransactionActive() == true {
+               t.Errorf("IsTransactionActive() returned true though it should be false")
+       }
+
+       coll.BeginTransaction()
+       coll.SaveBson(bytes)
+       coll.SaveBson(bytes)
+
+       if coll.IsTransactionActive() == false {
+               t.Errorf("IsTransactionActive() returned false though it should be true")
+       }
+
+       coll.CommitTransaction()
+
+       ct2, _ := coll.Count(`{}`)
+       if ct2 != 2 {
+               t.Errorf("transaction not committed successfully")
+       }
+}
+
+func TestOneSnippetIntroFromReadme(t *testing.T) {
+    jb, err := Open("addressbook", JBOWRITER | JBOCREAT | JBOTRUNC)
+    if err != nil {
+        os.Exit(1)
+    }
+    //Get or create collection 'contacts'
+    coll, _ := jb.CreateColl("contacts", nil)
+
+    //Insert one record:
+    //JSON: {'name' : 'Bruce', 'phone' : '333-222-333', 'age' : 58}
+    rec := map[string]interface{} {"name" : "Bruce", "phone" : "333-222-333", "age" : 58}
+    bsrec, _ := bson.Marshal(rec)
+    coll.SaveBson(bsrec)
+    fmt.Printf("\nSaved Bruce")
+
+    //Now execute query
+    res, _ := coll.Find(`{"name" : {"$begin" : "Bru"}}`) //Name starts with 'Bru' string
+    fmt.Printf("\n\nRecords found: %d\n", len(res))
+
+    //Now print the result set records
+    for _, bs := range res {
+        var m map[string]interface{}
+        bson.Unmarshal(bs, &m)
+        fmt.Println(m)
+    }
+
+    //Close database
+    jb.Close()
+}
+
+// long-running benchmarks to identify possible memory leaks
+
+func BenchmarkSavingAndCounting(b *testing.B) {
+       ejdb, _ := Open("/tmp/mybench.ejdb", JBOWRITER|JBOCREAT|JBOTRUNC|JBOTSYNC)
+       coll, _ := ejdb.CreateColl("BenchmarkCollection", nil)
+       bytes, _ := bson.Marshal(make_test_type())
+
+       for i := 0; i < 100000; i++ {
+               coll.SaveBson(bytes)
+       }
+
+       coll.Count(`{"s" : "mystring"}`)
+
+       ejdb.Close()
+}
+
+func BenchmarkCounting(b *testing.B) {
+       ejdb, _ := Open("/tmp/mybench2.ejdb", JBOWRITER|JBOCREAT|JBOTRUNC|JBOTSYNC)
+       coll, _ := ejdb.CreateColl("BenchmarkCollection", nil)
+       bytes, _ := bson.Marshal(make_test_type())
+       coll.SaveBson(bytes)
+       coll.Count(`{"s" : "mystring"}`)
+       ejdb.Close()
+}
diff --git a/goejdb/src/github.com/mkilling/ejdb/goejdb/ejquery.go b/goejdb/src/github.com/mkilling/ejdb/goejdb/ejquery.go
new file mode 100644 (file)
index 0000000..a5f85ce
--- /dev/null
@@ -0,0 +1,98 @@
+package goejdb
+
+// #cgo LDFLAGS: -ltcejdb
+// #include "../../../../../../tcejdb/ejdb.h"
+import "C"
+
+import "unsafe"
+
+const (
+       JBQRYCOUNT   = C.JBQRYCOUNT
+       JBQRYFINDONE = C.JBQRYFINDONE
+)
+
+type EjQuery struct {
+       ptr  *[0]byte
+       ejdb *Ejdb
+}
+
+// EJDB_EXPORT EJQ* ejdbcreatequery(EJDB *jb, bson *qobj, bson *orqobjs, int orqobjsnum, bson *hints);
+func (ejdb *Ejdb) CreateQuery(query string, queries ...string) (*EjQuery, *EjdbError) {
+       query_bson := bson_from_json(query)
+       defer C.bson_destroy(query_bson)
+
+       orqueries_count := len(queries)
+       orqueries := C.malloc((C.size_t)(unsafe.Sizeof(new(C.bson))) * C.size_t(orqueries_count))
+       defer C.free(orqueries)
+       ptr_orqueries := (*[maxslice]*C.bson)(orqueries)
+       for i, q := range queries {
+               (*ptr_orqueries)[i] = bson_from_json(q)
+               defer C.bson_destroy((*ptr_orqueries)[i])
+       }
+
+       q := C.ejdbcreatequery(ejdb.ptr, query_bson, (*C.bson)(orqueries), C.int(len(queries)), nil)
+       if q == nil {
+               return nil, ejdb.check_error()
+       } else {
+               return &EjQuery{ptr: q, ejdb: ejdb}, nil
+       }
+}
+
+// EJDB_EXPORT EJQ* ejdbqueryhints(EJDB *jb, EJQ *q, const void *hintsbsdata);
+func (q *EjQuery) SetHints(hints string) *EjdbError {
+       bsdata := bson_from_json(hints).data
+       ret := C.ejdbqueryhints(q.ejdb.ptr, q.ptr, unsafe.Pointer(bsdata))
+       if ret == nil {
+               return q.ejdb.check_error()
+       } else {
+               return nil
+       }
+}
+
+func (q *EjQuery) Execute(coll *EjColl) ([][]byte, *EjdbError) {
+       // execute query
+       var count C.uint32_t
+       res := C.ejdbqryexecute(coll.ptr, q.ptr, &count, 0, nil)
+       defer C.ejdbqresultdispose(res)
+       err := coll.ejdb.check_error()
+
+       // return results
+       ret := make([][]byte, 0)
+       for i := 0; i < int(count); i++ {
+               var size C.int
+               bson_blob := C.ejdbqresultbsondata(res, C.int(i), &size)
+               ret = append(ret, ((*[maxslice]byte)(bson_blob))[:int(size)])
+       }
+
+       return ret, err
+}
+
+func (q *EjQuery) ExecuteOne(coll *EjColl) (*[]byte, *EjdbError) {
+       // execute query
+       var count C.uint32_t
+       res := C.ejdbqryexecute(coll.ptr, q.ptr, &count, JBQRYFINDONE, nil)
+       defer C.ejdbqresultdispose(res)
+       err := coll.ejdb.check_error()
+
+       // return results
+       if count == 0 {
+               return nil, err
+       } else {
+               var size C.int
+               bson_blob := C.ejdbqresultbsondata(res, 0, &size)
+               ret := ((*[maxslice]byte)(bson_blob))[:int(size)]
+               return &ret, err
+       }
+}
+
+func (q *EjQuery) Count(coll *EjColl) (int, *EjdbError) {
+       var count C.uint32_t
+       C.ejdbqryexecute(coll.ptr, q.ptr, &count, JBQRYCOUNT, nil)
+       err := coll.ejdb.check_error()
+       return int(count), err
+}
+
+// EJDB_EXPORT void ejdbquerydel(EJQ *q);
+func (q *EjQuery) Del() {
+       C.ejdbquerydel(q.ptr)
+}
diff --git a/goejdb/src/github.com/mkilling/ejdb/goejdb/util.go b/goejdb/src/github.com/mkilling/ejdb/goejdb/util.go
new file mode 100644 (file)
index 0000000..64d380d
--- /dev/null
@@ -0,0 +1,67 @@
+package goejdb
+
+// #cgo LDFLAGS: -ltcejdb
+// #include "../../../../../../tcejdb/ejdb.h"
+import "C"
+
+import (
+       "encoding/json"
+       "labix.org/v2/mgo/bson"
+       "unsafe"
+)
+
+func bson_oid_from_string(oid *string) *C.bson_oid_t {
+       if oid == nil {
+               return nil
+       }
+
+       c_oid := C.CString(*oid)
+       defer C.free(unsafe.Pointer(c_oid))
+
+       ret := new(C.bson_oid_t)
+       C.bson_oid_from_string(ret, c_oid)
+       return ret
+}
+
+func bson_oid_to_string(oid *C.bson_oid_t) string {
+       var c_str [25]C.char
+       char_ptr := (*C.char)(unsafe.Pointer(&c_str))
+       C.bson_oid_to_string(oid, char_ptr)
+       return C.GoString(char_ptr)
+}
+
+func bson_to_byte_slice(bson *C.bson) []byte {
+       size := int(C.bson_size(bson))
+       data := C.bson_data(bson)
+       ptr_data := (*[maxslice]byte)(unsafe.Pointer(data))
+       return (*ptr_data)[:size]
+}
+
+func bson_from_byte_slice(bsdata []byte) *C.bson {
+       c_bson := new(C.bson)
+       //C.bson_init(c_bson)
+
+       buff := C.malloc(C.size_t(len(bsdata)))
+       ptr_buff := (*[maxslice]byte)(unsafe.Pointer(buff))
+       for i := 0; i < len(bsdata); i++ {
+               (*ptr_buff)[i] = bsdata[i]
+       }
+
+       C.bson_init_finished_data(c_bson, (*C.char)(buff))
+       return c_bson
+}
+
+func bson_from_json(j string) *C.bson {
+       var m map[string]interface{}
+       json.Unmarshal(([]byte)(j), &m)
+       bytes, _ := bson.Marshal(&m)
+       return bson_from_byte_slice(bytes)
+}
+
+func bson_destroy(bson *C.bson) {
+       C.bson_destroy(bson)
+}
+
+func bson_del(bson *C.bson) {
+       C.bson_del(bson)
+}