Skip to content

Commit

Permalink
simplify library
Browse files Browse the repository at this point in the history
  • Loading branch information
NoBypass committed Aug 7, 2024
1 parent edd49cd commit 132494b
Show file tree
Hide file tree
Showing 28 changed files with 1,147 additions and 1,140 deletions.
252 changes: 62 additions & 190 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,221 +1,93 @@
# SurGo
A simple sqlx-like library for using SurrealDB in Go.

**Table of Contents**
* [Installation](#installation)
* [To-Do](#to-do)
* [Connecting to a database](#connecting-to-a-database)
* [Configure the connection](#configure-the-connection)
* [Querying](#querying)
* [Scanning](#scanning)
* [Exec](#exec)
* [MustExec](#mustexec)
* [Result](#result)
* [IDs (Records)](#ids-records)
* [Normal/Array IDs](#normalarray-ids)
* [Ranged IDs](#ranged-ids)
* [Using multiple IDs](#using-multiple-ids)
* [Datetimes and Durations](#datetimes-and-durations)
<br />

<h1 align="center">
<img width=32 style="transform: translateY(6px)" src="https://raw.githubusercontent.com/surrealdb/icons/main/surreal.svg" />
&nbsp; Surgo &nbsp;
<img width=32 style="transform: translateY(6px)" src="https://raw.githubusercontent.com/surrealdb/icons/main/golang.svg" />
</h1>
<p align=center>QOL features and sqlx-like mappings for <code><a href="https://github.com/surrealdb/surrealdb.go">github.com/surrealdb/surrealdb.go</a></code></p>

<br />

<h2 align=center>Features</h2>
<p align=center><b>Features over the original library:</b></p>
<br />
<div style="display: flex; text-align: center; flex-direction: column; line-height: 0;">
<p>Simplified database connection</p>
<p>Ability to directly scan the result into a struct using sqlx-like syntax</p>
<p>A consistent <code>Result</code> type instead of using <code>interface{}</code></p>
<p>Consistent error handling</p>
<p>Up-to-date Documentation</p>
</div>

## Installation
Add the library to your go project using the following command:
```bash
go get github.com/NoBypass/surgo
```
**Make sure that your Go project runs on version 1.22 or later!**

## To-Do
- [x] Fix not parsing back from string to `time.Duration` in scanning
- [x] Add support for `Record` type
- [x] Parse surreal record id to `ID` type
- [x] Implement converting ID types
- [ ] Implement converting struct to query insertable string
- [ ] Allow pointers to structs for parameters
- [ ] Allow scanning to a nil pointer
- [ ] Improve/Update Docs
- [ ] Use SurrealDB variables in ranged IDs
- [ ] Improve error messages/errors in general

## Connecting to a database
Connect to a database and get a DB object and an error.

Example usage:
```go
db, err := surgo.Connect("127.0.0.1:8000")
```
> Make sure that your Go project runs on version 1.22 or later!
`MustConnect` works the same as [Connect](#Connect), but panics if an error occurs.
## Roadmap
- Automatically convert `time.Duration` and `time.Time` to the correct format for SurrealDB.
- Create a mapping function for each one which exists in the original surrealdb.go library (e.g. `Use`, `Merge`, etc.) but with the scan functionality like in the `Scan` function.
- Support `LiveNotifications` with the `Result` struct.
- Eventually remove the extra layer of the surrealdb.go library and directly use the websocket code.
- Keep the library up-to-date with the original library.
- (Maybe) Make it so that the library can be used with the `database/sql` package.

Example usage:
```go
db := surgo.MustConnect("127.0.0.1:8000")
```
## Documentation

### Configure the connection
To configure the database you can use the following functions:
- `User` - Set the username for the database.
- `Password` - Set the password for the connection.
- `Database` - Set the database to use.
- `Namespace` - Set the namespace to use.
- `CustomAgent` - Set a custom query agent to use.
### Connecting to the Database

Example usage with all functions:
```go
db, err := surgo.Connect("127.0.0.1:8000",
surgo.User("user"),
surgo.Password("password"),
surgo.Database("database"),
surgo.Namespace("namespace"))
db, err := surgo.Connect("ws://localhost:8000", &surgo.Credentials{
// any of these fields can be omitted if they are not needed
Namespace: "test",
Database: "default",
Username: "admin",
Password: "1234",
Scope: "public",
})
```

## Querying

### Scanning
Scan the data from the result of the query to a struct or slice of
structs. The first argument is the object to scan the result to, the
second argument is the query, and the third argument is the parameters.

If the given query string contains multiple queries, the last one will
be scanned to the given object. The values from the result are either
assigned to the fields of the given struct as lowercase names, or to their
`db` tags if they are present. If a struct or teh result contains fields
that are not present in the other, they will be ignored.

You can input a single struct or a slice of structs as the first argument.
What matters is that it has to be a pointer to either. Parameters work the
same way as in the [Exec](#Exec) function.

Example usage:
```go
type User struct {
CreatedAt int `db:"created_at"`
Name string
}
// panics if connection could not be established
db := surgo.MustConnect("ws://localhost:8000", &surgo.Credentials{
Username: "admin",
Password: "1234",
})

var user User
err := db.Scan(&user, "SELECT * FROM users:$1 WHERE", 1)
defer db.Close()
```

#### Behavior
The `dest` input must have the same structure as the expected return value
from the query. This means you will have to provide a slice even if you used
something like `LIMIT 1` and expect only one result. If you want to scan to a
single struct, you have to use the `ONLY` keyword in the query (or index the
returned array).

Since the scanning is that strict, you also have to play attention in other
cases like for example when mapping to a single variable. As an example, if
you use count in SurrealDB, the result will be:
```SQL
SELECT count() FROM test GROUP ALL
/* returns:
[
{
"count": 2
}
]
*/
```
### Querying the Database

What you could do here is to wrap the query in a `RETURN` statement and then
it will work just fine:
```SQL
RETURN (SELECT count() FROM test GROUP ALL)[0].count
-- returns: 2
```go
result, err := db.Query("SELECT * FROM ONLY $john", map[string]any{
"john": "users:john",
})
```

### Exec
Execute a query and return the result. The parameters can be just
normal values, a map, or a struct. If a map or a struct is used, the
keys or the fields of the map or struct will be used as the names of
the parameters. If the `db` tag is present for some fields it will be
used as the names of the parameters instead.
The result of the query above will be of type `[]surgo.Result`. The important fields of the `Result` struct are:
- `Data` which is of type `any` containing the data of the query (with a simple query like above a `map[string]any`).
- `Error` which is of type `error` containing the error of the query (if there is one).

The parameters can be represented with `$myvar` in the query string,
where `myvar` is the name of the parameter. This works when either
structs or maps are used but if normal values are used, you will have
to use the `$1`, `$2`, etc. syntax.
If you want to scan the result from such a query into a struct, you can use the `Result.Unmarshal` function:

Example usage:
```go
result, err := db.Exec("INSERT INTO users (name, age) VALUES ($name, $age)", map[string]any{
"name": "John",
"age": 25,
})

result, err := db.Exec("INSERT INTO users (name, age) VALUES ($1, $2)", "John", 25)

type User struct {
ID string
Name string
Age int
}
result, err := db.Exec("INSERT INTO users (name, age) VALUES ($name, $age)", User{
Name: "John",
Age: 25,
})
```

### MustExec
Works the same as [Exec](#Exec), but panics if an error occurs.

Example usage:
```go
result := db.MustExec("DEFINE TABLE users")
var john User
result[0].Unmarshal(&john)
```

### Result
The result of a query. It is returned by the [Exec](#Exec) and
[MustExec](#MustExec) functions.

```go
type Result struct {
Data any
Error error
Duration time.Duration
}
```

Data will contain either a `map[string]any` or a `[]map[string]any` depending
on the query. The other fields are self-explanatory.

### IDs (Records)
IDs are used to reference the data in the database. They are a big part of SurrealDB. There are multiple ways these
can be used. You can have just a normal ID and an array ID which contains multiple values that make up the ID. With
the latter, you can also use [ranged IDs](https://surrealdb.com/docs/surrealdb/surrealql/datamodel/ids/#record-ranges)
while querying the database.
### Directly Scanning the Result

#### Normal/Array IDs
Normal IDs can simply be used like this:
```go
db.Exec("SELECT * FROM foo:$", surgo.ID{"myid"}) // single value
// will be parsed to: SELECT * FROM foo:`myid`

db.Exec("SELECT * FROM foo:$", surgo.ID{"myid", 123}) // multiple values
// will be parsed to: SELECT * FROM foo:['myid', 123]
```

Notice that the ID was represented by a single `$` in the query string. This is done to separate the IDs from the other
parameters. You can still mix them with other parameters.

#### Ranged IDs
Ranged IDs can be used like this:
```go
db.Exec("SELECT * FROM foo:$", surgo.Range{surgo.ID{"myid", 123}, surgo.ID{"myid", 456}})
// will be parsed to: SELECT * FROM foo:['myid', 123]..['myid', 456]
```

#### Using multiple IDs
You can use multiple IDs in a single query, but you have to keep the same order in the parameters as in the query string.
While you can still mix them with other parameters, you have to play attention to their order when mixing them with
normal parameters (like `$1`, `$2` etc.) as their index might get mixed up.
```go
db.Exec("RELATE foo:$->edge->bar:$", surgo.ID{123}, surgo.ID{456}
// will be parsed to: RELATE foo:123->edge->bar:456
var john User
err := db.Scan(&john, "SELECT * FROM ONLY $john", map[string]any{
"john": "users:john",
})
```

### Datetimes and Durations
You can use the `time.Time` type for datetime values. The library will automatically convert them to the correct format
for SurrealDB. Some goes for the `time.Duration` type. It will be converted to the highest possible unit (e.g. seconds)
that keeps the precision. Both of these types can be used as parameters in query functions. It does not matter if they are used as normal values,
in maps, or in structs. When scanning if the destination field is of type `time.Time` and the source is a string, the library will try to parse
the string to a `time.Time` object.
92 changes: 0 additions & 92 deletions actions.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
Expand Down
Loading

0 comments on commit 132494b

Please # to comment.