duckgo is a DuckDB adapter for the lynktable storage abstraction. It lets lynkapi-based applications use an embedded DuckDB database as a table store, including schema migration.
- full
lynktable.Connectorimplementation over embedded DuckDB: Insert, Igsert, BatchIgsert, Update, Upsert, Delete, Count, Query, QueryRaw, Exec - diff-based schema migration via
SchemaSync: creates or alters tables and columns, adds, updates or drops indexes, and converges on repeated syncs - sequence-backed auto-increment columns (DuckDB has no native auto-increment): each
IncrAblecolumn gets a<table>__<col>_seqsequence withDEFAULT nextval(...) - inlining of allow-listed SQL function expressions (
NEXTVAL,COUNT,SUM,MIN,MAX,LENGTH) that DuckDB cannot bind as statement parameters - WHERE builder supporting both the dotted DSL (
"field.op") and raw SQL fragments
go get github.com/lynkdb/duckgoThe DuckDB driver ships prebuilt native libraries via duckdb-go-bindings (platform-specific modules in go.mod), so building and testing require CGO but no local DuckDB installation.
package main
import (
"fmt"
"github.com/lynkdb/duckgo"
"github.com/lynkdb/lynkapi/go/lynktable/modeler"
)
func main() {
db, err := duckgo.NewConnector("test.db")
if err != nil {
panic(err)
}
defer db.Close()
// declare a table and sync it into the database
tbl := modeler.NewTable("user", "", "")
id := modeler.NewColumn("id", "uint32", "", "")
id.IncrAble = true
tbl.AddColumn(id)
tbl.AddColumn(modeler.NewColumn("name", "varchar", "", ""))
tbl.AddColumn(modeler.NewColumn("created", "uint64", "", ""))
tbl.AddIndex(modeler.NewIndex(modeler.IndexTypePrimaryKey, []string{"id"}))
tbl.AddIndex(modeler.NewIndex(modeler.IndexTypeUnique, []string{"name"}))
schema := &modeler.Schema{}
schema.Tables = append(schema.Tables, tbl)
if err := db.Modeler().SchemaSync(schema); err != nil {
panic(err)
}
// insert a row; the id is generated by the sequence
if rs := db.Insert("user", map[string]any{
"id": "NEXTVAL('user__id_seq')",
"name": "Tom",
"created": uint64(1),
}); rs.Err() != nil {
panic(rs.Err())
}
// upsert on the primary key
if rs := db.Upsert("user",
map[string]any{"name": "Tom2"},
map[string]any{"id": 1},
); rs.Err() != nil {
panic(rs.Err())
}
// query with a filter
q := duckgo.NewQueryer().
From("user").
Select("id, name").
Limit(100)
q.Where().And("name.like", "Tom%")
rs := db.Query(q)
if rs.Err() != nil {
panic(rs.Err())
}
for ; rs.Valid(); rs.Next() {
fmt.Println(rs.Field("id").Uint32(), rs.Field("name").String())
}
}Filter.And / Filter.Or accept two input styles:
- dotted DSL
"field.op"with the operatorseq(default),ne,gt,ge,lt,le,likeandin, e.g.And("group.eq", 1),And("tags.in", "a", "b") - raw SQL fragments — any expression containing
(or?is passed through verbatim with its args appended as bind parameters, e.g.And("LENGTH(name) > ?", 2)
Or marks a single item as OR-joined; AND is the default connective.
- The connector opens DuckDB with
threads=1for predictable embedded behavior. - A
Queryeralways emits a LIMIT clause (default 1), so larger result sets need an explicit.Limit(). - Columns managed outside the modeler schemas (currently
lynk_version) are never dropped bySchemaSync. - To support another SQL function in bind-var inlining and quoting, add it to
dialectAllowFuncsindialect.go.
go test -v .Licensed under the Apache License, Version 2.0. See the LICENSE for details.