π DALgo
DALgo is a database abstraction layer for Go applications. It gives your business code one small, consistent API for records, queries, transactions, hooks, and schema-aware key mapping while letting the storage backend remain an implementation choice.
go get github.com/dal-go/dalgo
π― Why Use DALgo
DALgo is useful when an application needs stable data-access code without coupling the domain layer to Firestore, SQL, or a test-only database.
- Keep application logic independent from a concrete database client.
- Use the same record, query, and transaction shape across supported adapters.
- Test business logic with the built-in in-memory adapter.
- Add logging, validation, metrics, and other behavior through hooks.
- Model both document/key-value stores and relational tables through one key and schema abstraction.
DALgo does not try to hide every database difference. Adapters can return
dal.ErrNotSupported for capabilities their backend cannot provide. This keeps
the core API honest while still giving applications a shared path for the common
operations.
β‘ Quick Example
This example uses dalgo2memory, the built-in in-memory adapter. The same
application code can be written against dal.DB and supplied with another
adapter in production.
package main
import (
"context"
"fmt"
"github.com/dal-go/dalgo/dal"
"github.com/dal-go/dalgo/adapters/dalgo2memory"
)
type User struct {
Name string
Email string
}
func main() {
ctx := context.Background()
db := dalgo2memory.NewDB()
key := dal.NewKeyWithID("users", "u1")
if err := db.Set(ctx, dal.NewRecordWithData(key, &User{
Name: "Ada Lovelace",
Email: "[email protected]",
})); err != nil {
panic(err)
}
var user User
record := dal.NewRecordWithData(key, &user)
if err := db.Get(ctx, record); err != nil {
panic(err)
}
if record.Exists() {
fmt.Println(user.Email)
}
}
πͺ Typed Collections (Simplified API)
For everyday point CRUD you usually do not need to build keys, wrap records, and
type-assert data by hand. DALgo provides a generic, session-less
dal.Collection[K, T] handle (id type K, record type T) that returns typed
values directly. It is additive over the core API, uses no reflection of its
own, and works with every adapter.
type User struct {
Name string
Email string
}
// CollectionName (value receiver) names the collection.
func (User) CollectionName() string { return "users" }
// A Collection[K, T] holds no session, so declare it once and reuse it
// (e.g. as a package-level var). Here ids are strings (K = string).
var Users = dal.CollectionOf[string, User]()
func demo(ctx context.Context, db dal.DB) error {
// Writes go through a read-write transaction. Because dal.DB is not a
// WriteSession, calling a write terminal with a plain db is a compile error.
if err := db.RunReadwriteTransaction(ctx, func(ctx context.Context, tx dal.ReadwriteTransaction) error {
return Users.SetByID(ctx, tx, "u1", User{Name: "Ada Lovelace", Email: "[email protected]"})
}); err != nil {
return err
}
// Reads take a dal.ReadSession (a plain dal.DB satisfies it) and return T.
user, err := Users.GetData(ctx, db, "u1")
if err != nil {
return err // not-found is reported via dal.IsNotFound(err)
}
fmt.Println(user.Email)
return nil
}
The handle exposes the common operations as typed terminals:
- Reads:
GetData(one record βT),GetRecord(βdal.Record),GetRecordWithID(βdal.RecordWithID[K]),GetRecordWithDataAndID(βdal.RecordWithDataAndID[K, *T]),All(whole collection β[]T),First,Count,Exists. For interface-typed model data created by a factory, use the free functiondal.GetRecordWithIDIntoData(ctx, s, key, id, data), which decodes into the value you pass. - Writes:
Insert(generated id β*dal.Key),InsertWithID(known id),InsertRecord,SetByID(upsert),SetRecord,UpdateByID,UpdateByKey,DeleteByID,DeleteByKey, and batchInsertManyvia the opt-indal.ManyInserter[K, T]interface. For interface-typed model data, insert via the free functiondal.InsertRecordWithDataAndID(ctx, s, key, id, data)(the write twin ofGetRecordWithIDIntoData). - Composite / multi-field keys: pass
dal.WithKeyOptions(...)to the constructor, or build a*dal.Keywithdal.NewKeyWithFieldsand use the*ByKeyterminals. - Deprecated aliases:
Get/Set/Update/Deleteremain as thin delegators toGetData/SetByID/UpdateByID/DeleteByID. - Nesting:
In(parentKey)scopes the handle to a subcollection such asusers/u1/contacts. - Compile-time safety: read terminals take
dal.ReadSessionand write terminals takedal.WriteSession, so writes are only reachable insideRunReadwriteTransaction.
Standard database/sql vs DALgo
The same "read one user by id" written against the standard library and against a DALgo typed collection. The DALgo version is backend-agnostic: the identical code runs on Firestore, SQL, the filesystem, or the in-memory adapter.
Standard database/sql | DALgo typed collection |
|---|---|
|
|
π Core API
The main package is dal. It defines the interfaces application code
usually depends on:
dal.DBfor database-level reads, transactions, schema metadata, and adapter identity.dal.ReadSessionanddal.ReadwriteSessionfor read and write operations.dal.Recordanddal.Keyfor database records and hierarchical keys.dal.Queryfor structured and text queries.dal.Schemafor mapping DALgo keys to relational columns.
Most applications should accept dal.DB, dal.ReadSession, or
dal.ReadwriteSession in their own services instead of accepting a concrete
adapter type.
func LoadUser(ctx context.Context, db dal.ReadSession, id string) (*User, error) {
user := new(User)
record := dal.NewRecordWithData(dal.NewKeyWithID("users", id), user)
if err := db.Get(ctx, record); err != nil {
return nil, err
}
if !record.Exists() {
return nil, dal.ErrRecordNotFound
}
return user, nil
}
π³ Hierarchical Collections
DALgo keys can represent nested document paths, which maps naturally to
Firestore-style collections such as countries/ireland/cities/dublin.
countryKey := dal.NewKeyWithID("countries", "ireland")
cityKey := dal.NewKeyWithParentAndID(countryKey, "cities", "dublin")
err := db.Set(ctx, dal.NewRecordWithData(cityKey, &City{
Name: "Dublin",
Population: 592713,
}))
The same parent key can scope a query to a nested collection. For Firestore this
is the shape of a query under countries/ireland/cities.
ireland := dal.NewKeyWithID("countries", "ireland")
cities := dal.NewCollectionRef("cities", "", ireland)
q := dal.From(cities).NewQuery().
WhereField("Population", dal.GreaterThen, 100000).
OrderBy(dal.DescendingField("Population")).
SelectColumns(
dal.Column{Expression: dal.Field("Name")},
dal.Column{Expression: dal.Field("Population")},
)
π Queries
DALgo includes a structured query builder for common database-style reads:
filters, ordering, joins, column projection, and aggregation. Adapter support is
capability-based, so tests can share the same query shape and skip a backend
cleanly when it reports dal.ErrNotSupported.
q := dal.From(dal.NewRootCollectionRef("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)
Recent query capabilities include:
- Column projection through
SelectColumns. GROUP BY,HAVING, and aggregate functions such asCOUNT(*)andSUM.- Inner and left equi-joins in the structured query model.
- Source-qualified field references for joins and
ORDER BY. - Recordset readers with typed columns where the adapter supports columnar output.
π Transactions
Transactions use callback-style workers. This keeps transaction lifetime scoped and lets adapters implement retries or backend-specific transaction behavior.
err := db.RunReadwriteTransaction(ctx, func(ctx context.Context, tx dal.ReadwriteTransaction) error {
key := dal.NewKeyWithID("users", "u1")
return tx.Set(ctx, dal.NewRecordWithData(key, &User{Name: "Ada"}))
}, dal.TxWithMessage("create user u1"))
Transaction options can carry isolation-level requests and a human-readable message. Some adapters can surface the message in logs or backend history.
π§° Built-In Adapters
This repository includes:
dalgo2memory- in-memory DALgo database for tests, examples, local development, and query behavior verification. It supports schema registration, typed records, serialized storage, columnar storage, and mixed-modemap[string]anycolumnar storage.dalgo2fs- filesystem-backed adapter useful for simple local persistence and examples.
dalgo2memory is intentionally useful beyond trivial tests. It can run many
structured query features end to end, which makes it a practical default for
unit tests around application data access.
π Supported External Adapters
DALgo supports production use through separate adapter modules:
dalgo2firestorefor Google Cloud Firestore.dalgo2sqlfor SQL databases through Go SQL drivers, including SQLite, PostgreSQL, Oracle, and Microsoft SQL Server.dalgo2sqlitefor SQLite-specific schema, DDL, and concurrency-aware behavior on top of SQL support.
Deprecated BuntDB and BadgerDB adapters are not listed as supported production targets.
π¦ Packages
dal- core database abstraction, keys, records, sessions, transactions, queries, hooks, and schema mapping.dalgo2memory- built-in in-memory adapter.dalgo2fs- filesystem adapter.orm- object and collection mapping helpers.record- helpers for strongly typed record handling.recordset- row and column-oriented recordset structures.recordops- compare, diff, and render helpers for records.dbschema- schema definitions for collections, fields, indexes, constraints, and defaults.ddl- schema modification operations and applier interfaces.dtql- serialized query format and schema for DALgo queries.update- field update helpers.mocks- generated mocks for tests.
β Quality And Compatibility
The project is maintained with automated checks and adapter-oriented test coverage:
- CI runs build, tests,
go vet, and lint checks. - Core packages target full unit-test coverage.
- Shared end-to-end tests in
end2endexercise adapter behavior against the same scenarios. - Feature specifications in
spec/featuresdocument behavior that has been designed, implemented, and verified.
π Documentation
Start with these topic pages when you need more than the README:
π Projects Using DALgo
ingitdb- Git-native database tooling.inmemdb- in-memory database tooling.strongo/bots-framework- framework for building chatbots.DataTug- context-aware data viewer and collaborative query manager.
π€ Contributing
Contributions are welcome, especially adapter improvements, end-to-end coverage,
and documentation that makes backend capabilities clearer. See
CONTRIBUTING.md for project conventions.