Help us shape the future of Go! Take the 2026 Go Developer Survey

duckgo

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

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.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewConnector

func NewConnector(path string) (lynktable.Connector, error)

func NewFilter

func NewFilter() lynktable.Filter

func NewQueryer

func NewQueryer() lynktable.Queryer

Types

type Dialect

type Dialect struct {
	*lynktable.Base
}

func (*Dialect) DBName

func (dc *Dialect) DBName() string

func (*Dialect) Modeler

func (dc *Dialect) Modeler() modeler.Modeler

func (*Dialect) NewFilter

func (dc *Dialect) NewFilter() lynktable.Filter

func (*Dialect) NewQueryer

func (dc *Dialect) NewQueryer() lynktable.Queryer

func (*Dialect) QuoteStr

func (dc *Dialect) QuoteStr(str string) string

func (*Dialect) Upsert

func (dc *Dialect) Upsert(tableName string, item map[string]any, keys map[string]any) lynktable.Result

type DialectModeler

type DialectModeler struct {
	// contains filtered or unexported fields
}

func (*DialectModeler) ColumnDel

func (dc *DialectModeler) ColumnDel(tableName string, col *modeler.Column) error

func (*DialectModeler) ColumnList

func (dc *DialectModeler) ColumnList(tableName string) ([]*modeler.Column, error)

func (*DialectModeler) ColumnSet

func (dc *DialectModeler) ColumnSet(tableName string, col *modeler.Column) error

ColumnSet alters an existing column to match col. DuckDB does not accept NOT NULL / DEFAULT inside ALTER COLUMN ... TYPE, so each constraint is a separate statement.

func (*DialectModeler) ColumnSync

func (dc *DialectModeler) ColumnSync(tableName string, col *modeler.Column) error

func (*DialectModeler) IndexDel

func (dc *DialectModeler) IndexDel(tableName string, index *modeler.Index) error

func (*DialectModeler) IndexList

func (dc *DialectModeler) IndexList(tableName string) ([]*modeler.Index, error)

func (*DialectModeler) IndexSet

func (dc *DialectModeler) IndexSet(tableName string, index *modeler.Index) error

func (*DialectModeler) IndexSync

func (dc *DialectModeler) IndexSync(tableName string, index *modeler.Index) error

func (*DialectModeler) QuoteStr

func (dc *DialectModeler) QuoteStr(str string) string

func (*DialectModeler) Schema

func (dc *DialectModeler) Schema() (*modeler.Schema, error)

func (*DialectModeler) SchemaSync

func (dc *DialectModeler) SchemaSync(newds *modeler.Schema) error

func (*DialectModeler) TableExist

func (dc *DialectModeler) TableExist(tableName string) bool

func (*DialectModeler) TableList

func (dc *DialectModeler) TableList() ([]*modeler.Table, error)

func (*DialectModeler) TableSync

func (dc *DialectModeler) TableSync(table *modeler.Table) error

type Filter

type Filter struct {
	// contains filtered or unexported fields
}

func (*Filter) And

func (fr *Filter) And(expr string, args ...any) lynktable.Filter

And appends a condition joined with AND, the default connective. An expr containing '(' or '?' is taken as a raw SQL fragment; anything else is parsed as the dotted DSL "field.op", with the operator defaulting to eq.

func (*Filter) Or

func (fr *Filter) Or(expr string, args ...any) lynktable.Filter

Or appends a condition joined with OR; expr is interpreted as in And.

func (*Filter) Parse

func (fr *Filter) Parse() (where string, params []any)

func (*Filter) Reset

func (fr *Filter) Reset() lynktable.Filter

type Queryer

type Queryer struct {
	// contains filtered or unexported fields
}

func (*Queryer) From

func (q *Queryer) From(s string) lynktable.Queryer

func (*Queryer) Group

func (q *Queryer) Group(s string) lynktable.Queryer

func (*Queryer) Limit

func (q *Queryer) Limit(num int64) lynktable.Queryer

func (*Queryer) Offset

func (q *Queryer) Offset(num int64) lynktable.Queryer

func (*Queryer) Order

func (q *Queryer) Order(s string) lynktable.Queryer

func (*Queryer) Parse

func (q *Queryer) Parse() (sql string, params []any)

func (*Queryer) Select

func (q *Queryer) Select(s string) lynktable.Queryer

func (*Queryer) SetFilter

func (q *Queryer) SetFilter(fr lynktable.Filter)

func (*Queryer) Where

func (q *Queryer) Where() lynktable.Filter

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL