An open API service indexing awesome lists of open source software.

https://github.com/hebertcisco/gdo

A typed database access library for Gleam.
https://github.com/hebertcisco/gdo

data-access data-access-layer data-access-library data-access-object database gleam gleam-lang gleam-language gleam-library library prepared-statements sql sql-query sqlite sqlite-database transactions

Last synced: 2 days ago
JSON representation

A typed database access library for Gleam.

Awesome Lists containing this project

README

          

# gdo

[![test](https://github.com/hebertcisco/gdo/actions/workflows/test.yml/badge.svg)](https://github.com/hebertcisco/gdo/actions/workflows/test.yml)

`gdo` is a typed database access library for Gleam.

It provides a small, functional API for:

- opening SQLite and MySQL connections
- executing prepared statements
- reading rows through typed decoders
- running explicit transactions
- handling failures through structured errors

## Features

- idiomatic Gleam API
- `Result`-based error handling
- SQLite and MySQL drivers
- positional and named parameters
- prepared statement workflow
- explicit transaction control
- typed row decoding helpers
- structured connection, query, transaction, and decode errors

## Installation

```sh
gleam add gdo
```

## Quick Start

```gleam
import gdo
import gdo/connection
import gdo/decode
import gdo/error.{type Error}
import gdo/result
import gdo/value.{Int, Positional, String}
import gleam/option.{None, Some}

pub fn main() -> Result(Nil, Error) {
use db <- result.try(gdo.open_sqlite("file:app.sqlite"))

use _ <- result.try(
connection.exec(
db,
"create table if not exists users (id integer primary key, name text not null)",
[],
),
)

use insert_result <- result.try(
connection.exec(db, "insert into users (id, name) values (?, ?)", [
Positional(Int(1)),
Positional(String("Ana")),
]),
)

let last_id = result.last_insert_id(insert_result)
let _ = last_id

use maybe_row <- result.try(
connection.query_one(db, "select id, name from users where id = ?", [
Positional(Int(1)),
]),
)

case maybe_row {
Some(current_row) -> {
let decoder =
decode.map2(
decode.column_at(0, using: decode.int()),
decode.column_at(1, using: decode.string()),
with: fn(id, name) { #(id, name) },
)

let assert Ok(#(id, name)) = decode.decode(current_row, using: decoder)
let _ = #(id, name)
connection.close(db)
}

None -> connection.close(db)
}
}
```

## Connection Workflow

Use `gdo.open_sqlite` for embedded SQLite databases, or `gdo.open_mysql` for
network MySQL connections. When you want to build the configuration explicitly,
use `connection.open` with a driver-specific config helper.

```gleam
import gdo
import gdo/connection

pub fn open_database() {
let assert Ok(db) =
gdo.sqlite_config("file:app.sqlite")
|> connection.open

assert connection.in_transaction(db) == False
}
```

MySQL configuration follows the same shape, while using the shared server
connection primitives under the hood:

```gleam
import gdo
import gdo/connection

pub fn open_mysql_database() {
let assert Ok(db) =
gdo.mysql_config("127.0.0.1", 3306, "app", "root", "secret")
|> connection.with_option("connect_timeout", "2000")
|> connection.open

assert connection.in_transaction(db) == False
}
```

For one-shot calls there are root helpers for both drivers:

- `gdo.exec_sqlite`
- `gdo.query_one_sqlite`
- `gdo.query_all_sqlite`
- `gdo.exec_mysql`
- `gdo.query_one_mysql`
- `gdo.query_all_mysql`

These helpers open a connection for the operation and return the typed result
directly.

## Statement Workflow

Prepare statements when you want to reuse SQL, validate placeholder style once,
or keep execution and reading separate.

```gleam
import gdo
import gdo/connection
import gdo/statement
import gdo/value.{Named, String}

pub fn find_user_by_email() {
let assert Ok(db) = gdo.open_sqlite("file:app.sqlite")
let assert Ok(stmt) =
connection.prepare(db, "select id, email from users where email = :email")

let assert Ok(Some(current_row)) =
statement.query_one(stmt, [Named("email", String("ana@example.com"))])

let _ = current_row
}
```

`gdo` supports:

- positional placeholders: `?`
- named placeholders: `:name`

Mixing placeholder styles in the same statement is rejected during preparation.

## Transaction Workflow

Transactions are explicit and keep the connection immutable from the caller's
point of view.

```gleam
import gdo
import gdo/connection
import gdo/value.{Int, Positional, String}

pub fn create_user() {
let assert Ok(db) = gdo.open_sqlite("file:app.sqlite")
let assert Ok(db) = connection.begin(db)

let assert Ok(_) =
connection.exec(db, "insert into users (id, name) values (?, ?)", [
Positional(Int(1)),
Positional(String("Ana")),
])

let assert Ok(db) = connection.commit(db)
let _ = db
}
```

Use `connection.rollback` when the unit of work should be discarded.

## Row Decoding

Rows can be inspected directly with `row.get` and `row.get_at`, or decoded into
application values with `gdo/decode`.

```gleam
import gdo/decode

pub fn user_decoder() {
decode.map2(
decode.column_at(0, using: decode.int()),
decode.column_at(1, using: decode.string()),
with: fn(id, name) { User(id:, name:) },
)
}

pub type User {
User(id: Int, name: String)
}
```

Available decoders include:

- `decode.int`
- `decode.float`
- `decode.bool`
- `decode.string`
- `decode.bytes`
- `decode.nullable`
- `decode.map`
- `decode.map2`
- `decode.map3`

## Error Model

`gdo` keeps failures inside the public `Error` type:

- `ConnectionError`
- `QueryError`
- `TransactionError`
- `DecodeError`
- `UnsupportedFeature`
- `InvalidConfiguration`

Helpers in `gdo/error` expose the common fields:

- `error.message`
- `error.code`
- `error.sqlstate`

Typical handling looks like this:

```gleam
import gdo
import gdo/error

pub fn run_query() {
case gdo.open_sqlite("file:app.sqlite") {
Ok(_) -> Nil
Error(err) -> {
let _message = error.message(err)
let _code = error.code(err)
let _sqlstate = error.sqlstate(err)
Nil
}
}
}
```

## SQLite Notes

Current SQLite support includes:

- real connection open and close
- prepared statement execution
- reads through `query_one` and `query_all`
- transaction operations
- `last_insert_id`
- driver error mapping into `gdo/error`

Current SQLite limitation:

- the JavaScript SQLite path is currently Deno-specific through `sqlight`
- result rows are most reliable through positional access and
`decode.column_at`. The current SQLite path does not yet expose real column
names from backend metadata, so rows returned from queries use synthetic
column names internally.

## MySQL Notes

Current MySQL support includes:

- network connection lifecycle
- prepared execution through the shared statement API
- reads through `query_one` and `query_all`
- transaction operations
- `last_insert_id`
- driver error mapping with MySQL code and SQLSTATE when available

Current MySQL limitations:

- the first MySQL runtime path is Erlang-only
- the JavaScript target returns an explicit unsupported-driver error for MySQL
- named placeholders are rewritten to positional execution before reaching the
MySQL client runtime
- multiple result sets are not exposed through the current `gdo` query API

## Contributing

Contributions are welcome. Read [CONTRIBUTING.md](./CONTRIBUTING.md),
[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), and [SECURITY.md](./SECURITY.md)
before opening a pull request.

## License

This project is licensed under the Apache License 2.0. See [LICENSE](./LICENSE).

## Development

From the `gdo` directory:

```sh
gleam test
gleam format --check src test
```