Storage-agnostic logic
Write business code against dal.DB. Decide later whether it lives in Postgres, Firestore, or a JSON file on disk.
DALgo is a Database Abstraction Layer for Go. Write your domain logic once against a single, well-tested interface — swap Postgres for Firestore, SQLite for Datastore, an in-memory store for a real one — without touching your business code.
Six reasons developers reach for DALgo on day one.
Write business code against dal.DB. Decide later whether it lives in Postgres, Firestore, or a JSON file on disk.
One small, opinionated API replaces piles of driver-specific boilerplate. Records, keys, queries, transactions — uniform across every adapter.
Swap a production driver for the built-in dalgo2memory adapter in tests. Need mocks instead? The generated mocks package ships in the core repo.
Because every operation goes through one interface, observability and audit logging are a wrapper — not a refactor.
Every official adapter is validated against the same end-to-end test suite. Conformance is the contract.
Read-only and read-write transactions are first-class. Pass a closure, get atomicity — without leaking driver-specific transaction types into your domain.
Two adapters ship inside the core module; the rest span SQL, NoSQL, file, and Git-versioned storage.
The interface itself. dal, orm, record, update, transactions, errors.
In-memory adapter shipped in the core module — the default store for tests, examples, and local development. Runs most structured-query features end to end.
github.com/dal-go/dalgoBuilt-in adapter mapping the OS file system to DALgo records — handy for demos and simple local persistence.
github.com/dal-go/dalgoAdapter for the standard database/sql — Postgres, MySQL, MSSQL, anything compliant.
SQLite-specific driver: schema, DDL, concurrency-aware operations on top of dalgo2sql.
For Google Cloud Firestore — a globally distributed NoSQL document database.
github.com/dal-go/dalgo2firestoreFor Google Cloud Datastore — Firestore's predecessor, still widely deployed on App Engine.
github.com/dal-go/dalgo2datastoreFor BuntDB — an embeddable in-memory key/value store. Deprecated: no longer a supported production target.
github.com/dal-go/dalgo2buntdbFor BadgerDB — an embeddable, persistent key-value database in pure Go. Deprecated: no longer a supported production target.
github.com/dal-go/dalgo2badgerPersist data on the local filesystem in human-readable formats — JSON, YAML, CSV.
github.com/dal-go/dalgo2filesVersioned, auditable storage backed by a Git repository — every write is a commit.
github.com/dal-go/dalgo2gitAdapter for inGitDB — a Git-native database with branchable, mergeable data. Lives in the inGitDB CLI.
github.com/ingitdb/ingitdb-cliA working example in fewer than 40 lines.
Install the core module. The built-in dalgo2memory adapter ships with it — no extra dependency to get started.
Open a database. The returned dal.DB is the only type your app needs to know about.
Define a record, set its data, then read it back by key.
Later, swap dalgo2memory for dalgo2sqlite or dalgo2firestore. Your handlers don't change.
// $ go get github.com/dal-go/dalgo
package main
import (
"context"
"fmt"
"github.com/dal-go/dalgo/dal"
"github.com/dal-go/dalgo/adapters/dalgo2memory"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
ctx := context.Background()
db := dalgo2memory.NewDB()
key := dal.NewKeyWithID("users", "u1")
user := &User{Name: "Ada Lovelace", Email: "ada@example.com"}
if err := db.Set(ctx, dal.NewRecordWithData(key, user)); err != nil {
panic(err)
}
var got User
record := dal.NewRecordWithData(key, &got)
if err := db.Get(ctx, record); err != nil {
panic(err)
}
if record.Exists() {
fmt.Println(got.Email)
}
}
A generic, session-less dal.Collection[T] handle returns typed values directly — no key wrangling, no record wrapping, no type assertions. It is additive over the core API and works with every adapter.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
// CollectionName (value receiver) names the collection.
func (User) CollectionName() string { return "users" }
// A Collection[T] holds no session — declare it once and reuse it.
var Users = dal.CollectionOf[User]()
func demo(ctx context.Context, db dal.DB) error {
// Writes take a WriteSession — only reachable inside a transaction.
if err := db.RunReadwriteTransaction(ctx,
func(ctx context.Context, tx dal.ReadwriteTransaction) error {
return Users.Set(ctx, tx, "u1",
User{Name: "Ada Lovelace", Email: "ada@example.com"})
}); err != nil {
return err
}
// Reads take a ReadSession (a plain dal.DB works) and return T.
user, err := Users.Get(ctx, db, "u1")
if err != nil { // not-found via dal.IsNotFound(err)
return err
}
fmt.Println(user.Email)
return nil
}
The same handle also offers Insert (generated id), InsertWithID,
All, First, Count, Exists,
Update, Delete, batch InsertMany, and
In(parentKey) for nested collections.
database/sql vs DALgoThe same operations written against the standard library and against DALgo. The DALgo version is backend-agnostic — identical code runs on Firestore, SQL, the filesystem, or the in-memory adapter.
Read one user by id — and return a typed value directly.
// Standard library: hand-written SQL + Scan.
type User struct {
ID int
Name string
Email string
}
func GetUser(ctx context.Context, db *sql.DB, id int) (*User, error) {
row := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = ?", id)
u := &User{}
err := row.Scan(
&u.ID,
&u.Name,
&u.Email,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return u, nil
}
// DALgo: one typed call, any backend.
type User struct {
ID int
Name string
Email string
}
func GetUser(ctx context.Context, db dal.DB, id int) (User, error) {
return dal.CollectionAt[User]("users").Get(ctx, db, id)
}
In one transaction: load the user; if not banned, flag them and add a Ban record.
// Standard library: manual BEGIN / COMMIT / ROLLBACK.
type User struct {
ID int
Name string
Email string
IsBanned bool
}
type Ban struct {
UserID int
BanTimestamp time.Time
}
func BanUser(ctx context.Context, db *sql.DB, id int) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
var u User
err = tx.QueryRowContext(ctx,
"SELECT id, name, email, is_banned FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Name, &u.Email, &u.IsBanned)
if err != nil {
return err
}
if u.IsBanned {
return nil
}
if _, err = tx.ExecContext(ctx,
"UPDATE users SET is_banned = TRUE WHERE id = ?", id); err != nil {
return err
}
if _, err = tx.ExecContext(ctx,
"INSERT INTO bans (user_id, ban_timestamp) VALUES (?, ?)",
id, time.Now()); err != nil {
return err
}
return tx.Commit()
}
// DALgo: one closure, the driver runs the transaction.
type User struct {
ID int
Name string
Email string
IsBanned bool
}
type Ban struct {
UserID int
BanTimestamp time.Time
}
func BanUser(ctx context.Context, db dal.DB, id int) error {
return db.RunReadwriteTransaction(ctx,
func(ctx context.Context, tx dal.ReadwriteTransaction) error {
users := dal.CollectionAt[User]("users")
user, err := users.Get(ctx, tx, id)
if err != nil {
return err
}
if user.IsBanned {
return nil
}
if err = users.Update(ctx, tx, id,
[]update.Update{update.ByFieldName("IsBanned", true)}); err != nil {
return err
}
bans := dal.CollectionAt[Ban]("bans")
_, err = bans.Insert(ctx, tx, Ban{UserID: id, BanTimestamp: time.Now()})
return err
})
}
Strict enough to keep every adapter honest.
Every DALgo adapter — SQL, NoSQL, embedded, filesystem — implements the same
dal.DB contract. Read operations live on
ReadSession; mutating operations live on
ReadwriteSession. Transactions are closures: pass a function, the
driver gives you a session, your code never sees a driver-specific handle.
dal.ErrNotSupported — explicit failure beats silent nonsense.package dal
type DB interface {
ID() string
Adapter() Adapter
Schema() Schema
TransactionCoordinator
ReadSession
ConcurrencyAware
}
type TransactionCoordinator interface {
RunReadonlyTransaction(ctx context.Context,
f ROTxWorker, opts ...TransactionOption) error
RunReadwriteTransaction(ctx context.Context,
f RWTxWorker, opts ...TransactionOption) error
}
type ReadSession interface {
Get(ctx context.Context, record Record) error
GetMulti(ctx context.Context, records []Record) error
ExecuteQueryToRecordsReader(ctx context.Context,
query Query) (RecordsReader, error)
}
type ReadwriteSession interface {
ReadSession
Insert(ctx context.Context, r Record, opts ...InsertOption) error
Set(ctx context.Context, r Record) error
Update(ctx context.Context, key *Key,
updates []update.Update, pre ...Precondition) error
Delete(ctx context.Context, key *Key) error
}
A structured query builder, hierarchical collections, and schema-aware key mapping — capability-based across adapters.
DALgo ships a structured query builder for the common database reads: filters,
ordering, LIMIT, column projection, GROUP BY /
HAVING, aggregates like COUNT(*) and SUM,
and inner / left equi-joins. Keys can model nested document paths such as
countries/ireland/cities/dublin, and a parent key scopes a query to a
nested collection.
dal.ErrNotSupported, so tests share one query shape and skip cleanly. The serialized query format lives in DTQL.// Query the largest cities, projecting two columns.
cities := dal.NewRootCollectionRef("cities", "")
q := dal.From(cities).NewQuery().
WhereField("Country", dal.Equal, "IE").
OrderBy(dal.DescendingField("Population")).
Limit(10).
SelectColumns(
dal.Column{Expression: dal.Field("Name")},
dal.Column{Expression: dal.Field("Population")},
)
records, err := dal.ExecuteQueryAndReadAllToRecords(ctx, q, db)
DALgo is MIT-licensed, production-tested, and waiting for you to break it. Contributions for new adapters and edge cases are welcome.