Lightweight Golang driver for ArangoDB

Arangolite Build Status Coverage Status Code Climate GoDoc

Arangolite is a lightweight ArangoDB driver for Go.

It focuses on pure AQL querying. See AranGO for a more ORM-like experience.

IMPORTANT: Looking for maintainers

I don't have as much time as I used to have and I am not as a frequent user of ArangoDB as I used to be. This project would definitely benefit from some new maintainers. Any PR is greatly appreciated.

Installation

To install Arangolite:

go get -u github.com/solher/arangolite

Basic Usage

package main

import (
  "context"
  "fmt"
  "log"

  "github.com/solher/arangolite"
  "github.com/solher/arangolite/requests"
)

type Node struct {
  arangolite.Document
}

func main() {
  ctx := context.Background()

  // We declare the database definition.
  db := arangolite.NewDatabase(
    arangolite.OptEndpoint("http://localhost:8529"),
    arangolite.OptBasicAuth("root", "rootPassword"),
    arangolite.OptDatabaseName("_system"),
  )

  // The Connect method does two things:
  // - Initializes the connection if needed (JWT authentication).
  // - Checks the database connectivity.
  if err := db.Connect(ctx); err != nil {
    log.Fatal(err)
  }

  // We create a new database.
  err := db.Run(ctx, nil, &requests.CreateDatabase{
    Name: "testDB",
    Users: []map[string]interface{}{
      {"username": "root", "passwd": "rootPassword"},
      {"username": "user", "passwd": "password"},
    },
  })
  if err != nil {
    log.Fatal(err)
  }

  // We sign in as the new created user on the new database.
  // We could eventually rerun a "db.Connect()" to confirm the connectivity.
  db.Options(
    arangolite.OptBasicAuth("user", "password"),
    arangolite.OptDatabaseName("testDB"),
  )

  // We create a new "nodes" collection.
  if err := db.Run(ctx, nil, &requests.CreateCollection{Name: "nodes"}); err != nil {
    log.Fatal(err)
  }

  // We declare a new AQL query with options and bind parameters.
  key := "48765564346"
  r := requests.NewAQL(`
    FOR n
    IN nodes
    FILTER n._key == @key
    RETURN n
  `, key).
    Bind("key", key).
    Cache(true).
    BatchSize(500) // The caching feature is unavailable prior to ArangoDB 2.7

  // The Run method returns all the query results of every pages
  // available in the cursor and unmarshal it into the given struct.
  // Cancelling the context cancels every running request. 
  nodes := []Node{}
  if err := db.Run(ctx, &nodes, r); err != nil {
    log.Fatal(err)
  }

  // The Send method gives more control to the user and doesn't follow an eventual cursor.
  // It returns a raw result object.
  result, err := db.Send(ctx, r)
  if err != nil {
    log.Fatal(err)
  }
  nodes = []Node{}
  result.UnmarshalResult(&nodes)

  for result.HasMore() {
    result, err = db.Send(ctx, &requests.FollowCursor{Cursor: result.Cursor()})
    if err != nil {
      log.Fatal(err)
    }
    tmp := []Node{}
    result.UnmarshalResult(&tmp)

    nodes = append(nodes, tmp...)
  }

  fmt.Println(nodes)
}

Document and Edge

// Document represents a basic ArangoDB document
type Document struct {
  // The document handle. Format: ':collection/:key'
  ID string `json:"_id,omitempty"`
  // The document's revision token. Changes at each update.
  Rev string `json:"_rev,omitempty"`
  // The document's unique key.
  Key string `json:"_key,omitempty"`
}

// Edge represents a basic ArangoDB edge
type Edge struct {
  Document
  // Reference to another document. Format: ':collection/:key'
  From string `json:"_from,omitempty"`
  // Reference to another document. Format: ':collection/:key'
  To string `json:"_to,omitempty"`
}

Transactions

Overview

Arangolite provides an abstraction layer to the Javascript ArangoDB transactions.

The only limitation is that no Javascript processing can be manually added inside the transaction. The queries can be connected using the Go templating conventions.

Usage

t := requests.NewTransaction([]string{"nodes"}, nil).
  AddAQL("nodes", `
    FOR n
    IN nodes
    RETURN n
`).
  AddAQL("ids", `
    FOR n
    IN {{.nodes}}
    RETURN n._id
`).
  Return("ids")

ids := []string{}
if err := db.Run(ctx, ids, t); err != nil {
  log.Fatal(err)
}

Graphs

Overview

AQL may be used for querying graph data. But to manage graphs, Arangolite offers a few specific requests:

  • CreateGraph to create a graph.
  • ListGraphs to list available graphs.
  • GetGraph to get an existing graph.
  • DropGraph to delete a graph.

Usage

// Check graph existence.
if err := db.Run(ctx, nil, &requests.GetGraph{Name: "graphName"}); err != nil {
  switch {
  case arangolite.IsErrNotFound(err):
    // If graph does not exist, create a new one.
    edgeDefinitions := []requests.EdgeDefinition{
      {
        Collection: "edgeCollectionName",
        From:       []string{"firstCollectionName"},
        To:         []string{"secondCollectionName"},
      },
    }
    db.Run(ctx, nil, &requests.CreateGraph{Name: "graphName", EdgeDefinitions: edgeDefinitions})
  default:
    log.Fatal(err)
  }
}

// List existing graphs.
list := &requests.GraphList{}
db.Run(ctx, list, &requests.ListGraphs{})
fmt.Printf("Graph list: %v\n", list)

// Destroy the graph we just created, and the related collections.
db.Run(ctx, nil, &requests.DropGraph{Name: "graphName", DropCollections: true})

Error Handling

Errors can be handled using the provided basic testers:

// IsErrInvalidRequest returns true when the database returns a 400.
func IsErrInvalidRequest(err error) bool {
  return HasStatusCode(err, 400)
}

// IsErrUnauthorized returns true when the database returns a 401.
func IsErrUnauthorized(err error) bool {
  return HasStatusCode(err, 401)
}

// IsErrForbidden returns true when the database returns a 403.
func IsErrForbidden(err error) bool {
  return HasStatusCode(err, 403)
}

// IsErrUnique returns true when the error num is a 1210 - ERROR_ARANGO_UNIQUE_CONSTRAINT_VIOLATED.
func IsErrUnique(err error) bool {
  return HasErrorNum(err, 1210)
}

// IsErrNotFound returns true when the database returns a 404 or when the error num is:
// 1202 - ERROR_ARANGO_DOCUMENT_NOT_FOUND
// 1203 - ERROR_ARANGO_COLLECTION_NOT_FOUND
func IsErrNotFound(err error) bool {
  return HasStatusCode(err, 404) || HasErrorNum(err, 1202, 1203)
}

Or manually via the HasStatusCode and HasErrorNum methods.

Contributing

Currently, very few methods of the ArangoDB HTTP API are implemented in Arangolite. Fortunately, it is really easy to add your own by implementing the Runnable interface. You can then use the regular Run and Send methods.

// Runnable defines requests runnable by the Run and Send methods.
// A Runnable library is located in the 'requests' package.
type Runnable interface {
  // The body of the request.
  Generate() []byte
  // The path where to send the request.
  Path() string
  // The HTTP method to use.
  Method() string
}

Please pull request when you implement some new features so everybody can use it.

License

MIT

Comments
  • Add AQL function requests

    Add AQL function requests

    Adds support for /_api/aqlfunction requests for manipulating AQL user functions (https://docs.arangodb.com/3.2/HTTP/AqlUserFunctions/).

    GET /_api/aqlfunction for some reason returns an array instead of the usual wrapper object, so I added a check in senders.go to prevent it from trying to unmarshal the raw response when it's an array (raw[0] != '[' seems like a bit of a hack, but I don't feel like it will cause any problems in practice).

  • Adding new commands

    Adding new commands

    I will extend the driver. I already started with an edge creation feature. But something worries me: in your readme you suggest to add everything in requests.go. So this file could become very long... I believe the code should be organised by around the business domain, and not the technical notions. By example the create edge request definition should be in edge.go. Do you agree with this?

  • Add Logger interface as input for OptLogging

    Add Logger interface as input for OptLogging

    By having an interface with the Print method only, it's possible to mock or redirect the output.

    Also, add LogNone constant is it's now necessary to pass an integer that is neither 0 or 1.

  • Basic example error

    Basic example error

    Hi

    In the basic example, in the readme, I believe you should switch these 2 lines:

    _, _ := db.Run(&arangolite.CreateCollection{Name: "nodes"})

    db.SwitchDatabase("testDB").SwitchUser("user", "password")

    Otherwise you create the collection in the wrong database

  • Quote in json value breaks the queries

    Quote in json value breaks the queries

    Hi Fabien

    When inserting a struct represeting a user, if any field of this user contains a simple quote, the resulting AQL query is invalid:

    My code is the following one

    userJsonBytes, err := json.Marshal(user)
    	if err != nil {
    		return err
    	}
    	userJson := string(userJsonBytes)
    
    	query := requests.NewAQL(` UPSERT { "id":"%s" } INSERT %s UPDATE %s IN %s `, user.ID, userJson, userJson, CollectionName)
    

    I probably could find a workaround by creating a dedicated json marshaller; but shouldn't the driver prevent this issue?

  • database.Run() response result (parsedResponse.Result) always empty

    database.Run() response result (parsedResponse.Result) always empty

    The response object returned by database.Send() is passed to database.followCursor() in the database.Run() function.

    When the response object is created in senders.Send(), the response's parsed (parsedResponse) attribute is unmarshalled.

    It seem the parsedResponse.Result attribute is never populated, and is simply an empty array.

    The problem then occurs in database.followCursor() where the call to r.RawResult() always returns an empty array, and hence, the result is not unmarshalled into the v input argument.

    I'm I doing something wrong?

  • Support cursors without goroutines

    Support cursors without goroutines

    Creating something like pagination seems expensive with this way.

    Say I run a query like FOR n IN nodes LIMIT 1000 RETURN 20, by using Run, all 1000 results if found in the DB are returned, and no cursor key to manage the request.

    Using RunAsync means having a goroutine followCursor created everytime there is more results in the cursor, and I am forced to get everything by checking if there is more just not to have hanging goroutine(s) in the background, or close the channel(meaning I have to begin going through the cursor again).

    If I had multiple clients in an application running the same query, I have to keep these goroutines even though none of them might be interested in going through the list of results.

    I propose we return the result.ID and let the user(developer) decide a proper method for getting the results.

  • Requests produce no result

    Requests produce no result

    When running the example below (derived from the readme example), I have two problems:

    • The node collection is not created.
    • the async request returns an empty array
    package main
    
    import (
      "encoding/json"
      "fmt"
    
      "github.com/solher/arangolite"
    )
    
    type Node struct {
      arangolite.Document
    }
    
    type MyNode struct {
      // The document handle. Format: ':collection/:key'
      ID string `json:"_id,omitempty"`
      // The document's revision token. Changes at each update.
      Rev string `json:"_rev,omitempty"`
      // The document's unique key.
      Key string `json:"_key,omitempty"`
    }
    
    func main() {
      db := arangolite.New().
        LoggerOptions(false, false, false).
        Connect("http://localhost:8529", "_system", "root", "")
    
      db.Run(&arangolite.CreateDatabase{
            Name: "testDB",
            Users: []map[string]interface{}{
                {"username": "root", "passwd": ""},
                {"username": "user", "passwd": ""},
            },
        })
      db.SwitchDatabase("testDB").SwitchUser("user", "")
    
      db.Run(&arangolite.CreateCollection{Name: "nodes"})
    
      // The Run method returns all the query results of every batches
      // available in the cursor as a slice of byte.
      key1 := "48765564346"
      key2 := "48765564347"
    
      _, err := remove(db, key1)
      _, err = remove(db, key2)
    
      _, err = insert(db, key1)
      _, err = insert(db, key2)
    
      if (err != nil) {
        return
      }
    
      q := arangolite.NewQuery(`
        FOR n
        IN nodes
        FILTER n._key == "%s"
        LIMIT 1
        RETURN n
      `, key1)
    
      async, asyncErr := db.RunAsync(q)
    
      if(asyncErr != nil){
        fmt.Printf("asyncErr %v", asyncErr)
      }
    
      nodes := []Node{}
      decoder := json.NewDecoder(async.Buffer())
    
      for async.HasMore() {
        batch := []Node{}
        decoder.Decode(batch)
        fmt.Printf("%v", decoder)
        nodes = append(nodes, batch...)
      }
    
      fmt.Printf("%v", nodes)
    }
    
    func remove(db *arangolite.DB, key string) ([]byte, error) {
      removeQuery := arangolite.NewQuery(`
          REMOVE  { _key: '%s' }  IN nodes
        `, key)
      res, err := db.Run(removeQuery)
      if(err != nil) {
        fmt.Printf("Remove error %v", err)
      }
      return res, err
    }
    
    func insert(db *arangolite.DB, key string) ([]byte, error) {
    
      node := MyNode{
         ID: "nodes/" + key,
         Rev: key,
         Key: key,
      }
    
      value, marshallErr := json.Marshal(node)
      if(marshallErr != nil) {
        fmt.Printf("Insertion error %v. Cannot convert %v to JSON", marshallErr, value)
        return nil, marshallErr
      }
      insertQuery := arangolite.NewQuery(`
          INSERT %s
          IN nodes
        `, value)
    
      insertResult, err := db.Run(insertQuery)
    
      if (err != nil) {
        fmt.Printf("Insertion error %v", err)
      }
      return insertResult, err
    }
    

    What did I get wrong?

  • Possibility to get []byte instead of umarshaled struct as result

    Possibility to get []byte instead of umarshaled struct as result

    We are using this lib and extracting large chunks of json. The problem is that these jsons does not have strict structure so they should be unmarshaled only to map[string]interface{} for latter use, but in our scenario we do not need to unmarshal json at all and raw []byte of json is ok. It would be nice:

    • To have func (db *Database) GetRaw(ctx context.Context, q Runnable) (json.RawMessage, error) or equivalent.

    Or

    • Make followCursor as exported so this can be easily achieved with Send.

    Any opinions?

  • Accessing error code from returned error

    Accessing error code from returned error

    When performing a CreateDatabase where the database already exists, the database returns error code 1207 (https://docs.arangodb.com/3.0/Manual/Appendix/ErrorCodes.html, ERROR_ARANGO_DUPLICATE_NAME).

    This error code is wrapped in a numberedError at https://github.com/solher/arangolite/blob/v2.0.1/database.go#L207

    However, at the next step, the numberedError is wrapped in a statusCodedError at https://github.com/solher/arangolite/blob/v2.0.1/database.go#L213

    The statusCodedError is what is returned by Send.

    My question is how should the error code be accessed?

    HasErrorNum will not work, since it requires a numberedError as input, and casting to statusCodedError to access the error attribute doesn't work, since statusCodedError is not exported.

    Current result of arangolite.HasStatusCode(err) is 409, but arangolite.HasErrorNum(e_status, 1207) gives false.

  • Add statistics requests

    Add statistics requests

    Adds support for /_admin/statistics and /_admin/statistics-description (https://docs.arangodb.com/3.2/HTTP/AdministrationAndMonitoring/#read-the-statistics).

    The JSON result of GetStatistics is structured in a way that's difficult to unmarshal with encoding/json, so I ended up writing a custom UnmarshalJSON function for it. Not sure if there's a cleaner way to accomplish this.

  • Make db fields public

    Make db fields public

    I trying to use this library as the basis for an ArangoDB migration tool. Creating a new database is harder than it should be since the DB properties are hidden. I'm sure I could get them with reflect, but I don't think that should be necessary. Would you be open to making the fields for NewDatabase public via an interface or just regular public?

  • How Do I Remove Edge

    How Do I Remove Edge

    Is this even possible using AQL only?

    From here it says that currently there is no way to do this via AQL. But arangolite doesn't seems to have a way to remove edge using graph management interface?

Qmgo - The Go driver for MongoDB. It‘s based on official mongo-go-driver but easier to use like Mgo.

Qmgo English | 简体中文 Qmgo is a Go driver for MongoDB . It is based on MongoDB official driver, but easier to use like mgo (such as the chain call). Qmg

Dec 28, 2022
Go driver for PostgreSQL over SSH. This driver can connect to postgres on a server via SSH using the local ssh-agent, password, or private-key.

pqssh Go driver for PostgreSQL over SSH. This driver can connect to postgres on a server via SSH using the local ssh-agent, password, or private-key.

Nov 6, 2022
Firebird RDBMS sql driver for Go (golang)

firebirdsql (Go firebird sql driver) Firebird RDBMS http://firebirdsql.org SQL driver for Go Requirements Firebird 2.5 or higher Golang 1.13 or higher

Dec 20, 2022
Golang driver for ClickHouse

ClickHouse Golang SQL database driver for Yandex ClickHouse Key features Uses native ClickHouse tcp client-server protocol Compatibility with database

Jan 8, 2023
Golang MySQL driver

Install go get github.com/vczyh/go-mysql-driver Usage import _ "github.com/vczyh

Jan 27, 2022
Mirror of Apache Calcite - Avatica Go SQL Driver

Apache Avatica/Phoenix SQL Driver Apache Calcite's Avatica Go is a Go database/sql driver for the Avatica server. Avatica is a sub-project of Apache C

Nov 3, 2022
Microsoft ActiveX Object DataBase driver for go that using exp/sql

go-adodb Microsoft ADODB driver conforming to the built-in database/sql interface Installation This package can be installed with the go get command:

Dec 30, 2022
Microsoft SQL server driver written in go language

A pure Go MSSQL driver for Go's database/sql package Install Requires Go 1.8 or above. Install with go get github.com/denisenkom/go-mssqldb . Connecti

Dec 26, 2022
Oracle driver for Go using database/sql

go-oci8 Description Golang Oracle database driver conforming to the Go database/sql interface Installation Install Oracle full client or Instant Clien

Dec 30, 2022
sqlite3 driver for go using database/sql

go-sqlite3 Latest stable version is v1.14 or later not v2. NOTE: The increase to v2 was an accident. There were no major changes or features. Descript

Jan 8, 2023
GO DRiver for ORacle DB

Go DRiver for ORacle godror is a package which is a database/sql/driver.Driver for connecting to Oracle DB, using Anthony Tuininga's excellent OCI wra

Jan 5, 2023
Go Sql Server database driver.

gofreetds Go FreeTDS wrapper. Native Sql Server database driver. Features: can be used as database/sql driver handles calling stored procedures handle

Dec 16, 2022
PostgreSQL driver and toolkit for Go

pgx - PostgreSQL Driver and Toolkit pgx is a pure Go driver and toolkit for PostgreSQL. pgx aims to be low-level, fast, and performant, while also ena

Jan 4, 2023
Pure Go Postgres driver for database/sql

pq - A pure Go postgres driver for Go's database/sql package Install go get github.com/lib/pq Features SSL Handles bad connections for database/sql S

Jan 2, 2023
Go language driver for RethinkDB
Go language driver for RethinkDB

RethinkDB-go - RethinkDB Driver for Go Go driver for RethinkDB Current version: v6.2.1 (RethinkDB v2.4) Please note that this version of the driver on

Dec 24, 2022
goriak - Go language driver for Riak KV
goriak - Go language driver for Riak KV

goriak Current version: v3.2.1. Riak KV version: 2.0 or higher, the latest version of Riak KV is always recommended. What is goriak? goriak is a wrapp

Nov 22, 2022
Mongo Go Models (mgm) is a fast and simple MongoDB ODM for Go (based on official Mongo Go Driver)
Mongo Go Models (mgm) is a fast and simple MongoDB ODM for Go (based on official Mongo Go Driver)

Mongo Go Models Important Note: We changed package name from github.com/Kamva/mgm/v3(uppercase Kamva) to github.com/kamva/mgm/v3(lowercase kamva) in v

Jan 2, 2023
The MongoDB driver for Go

The MongoDB driver for Go This fork has had a few improvements by ourselves as well as several PR's merged from the original mgo repo that are current

Jan 8, 2023
The Go driver for MongoDB
The Go driver for MongoDB

MongoDB Go Driver The MongoDB supported driver for Go. Requirements Installation Usage Bugs / Feature Reporting Testing / Development Continuous Integ

Dec 31, 2022