Skip to content

Repository files navigation

duckgo

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.

Features

  • full lynktable.Connector implementation 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 IncrAble column gets a <table>__<col>_seq sequence with DEFAULT 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

Installing

go get github.com/lynkdb/duckgo

The 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.

Getting Started

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 expressions

Filter.And / Filter.Or accept two input styles:

  • dotted DSL "field.op" with the operators eq (default), ne, gt, ge, lt, le, like and in, 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.

Notes

  • The connector opens DuckDB with threads=1 for predictable embedded behavior.
  • A Queryer always 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 by SchemaSync.
  • To support another SQL function in bind-var inlining and quoting, add it to dialectAllowFuncs in dialect.go.

Testing

go test -v .

Licensing

Licensed under the Apache License, Version 2.0. See the LICENSE for details.

About

Go client for DuckDB

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages