couac

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 30, 2026 License: Apache-2.0 Imports: 26 Imported by: 2

README

couac 🦆🛢️♭

couac logo

Go Reference

Couac is a Go library that provides a convenient, ergonomic wrapper around ADBC (Arrow Database Connectivity) for DuckDB. It simplifies database lifecycle management, connection tracking, query execution with Apache Arrow record batches, bulk ingestion with automatic schema evolution, safe compaction, and much more.

Features

Category Functions
Database lifecycle NewDuck, Close, Ping, Path, DriverPath
Connections Connect, ConnectAs, ConnectionCount, Close
Query execution Exec, QueryQueryResult, QueryRaw, Prepare, NewStatement
Transactions WithTransaction (auto commit/rollback with panic recovery)
Bulk ingestion Ingest, IngestMerge (schema evolution via UNION BY NAME), IngestReplace, IngestStream
Catalog metadata ObjectsCatalogTree (typed getters: Catalogs, Schemas, Tables, Columns, FindTable, TableExists, Constraints, Map), ObjectsMap, TableSchema, TableSchemaIn, TableTypes
System management Compact (safe disk reclamation), Checkpoint, ForceCheckpoint
Attach / Detach Attach (with ReadOnly, WithBlockSize, WithEncryptionKey options), Detach, CopyDatabase, Databases
Extensions & Secrets Extensions, InstallExtension, LoadExtension, Secrets, ExtensionsDir, SecretsDir
Configuration Set, Reset, Setting, Settings, LockConfiguration
Performance tuning SetMemoryLimit, SetThreads, SetTempDirectory, SetMaxTempDirectorySize, SetPreserveInsertionOrder
Introspection Describe, Summarize, ShowTables, ShowAllTables, Explain
Environment Version, Platform, UserAgent, DatabaseSize, StorageInfo
Profiling EnableProfiling, DisableProfiling, SetProfilingOutput
database/sql StdDB*sql.DB (bridge for ORMs, migration tools, test harnesses; supports parameterized queries with ? and $N placeholders)

Prerequisites

Couac uses the ADBC driver manager, which loads the DuckDB shared library at runtime. The recommended way to install the driver is via the dbc CLI:

# macOS / Linux
curl -fsSL https://dbc.columnar.tech/install.sh | bash

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://dbc.columnar.tech/install.ps1 | iex"

# Or with pipx (cross-platform)
pipx install dbc

# Then install the DuckDB driver
dbc install duckdb

Helper scripts are provided in the scripts/ directory:

  • scripts/install-dbc.sh — installs dbc + DuckDB driver on macOS/Linux
  • scripts/install-dbc.ps1 — installs dbc + DuckDB driver on Windows

Install

go get github.com/loicalleyne/couac@latest

Quick start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/apache/arrow-go/v18/arrow"
    "github.com/apache/arrow-go/v18/arrow/array"
    "github.com/apache/arrow-go/v18/arrow/memory"
    "github.com/loicalleyne/couac"
)

func main() {
    // Open an in-memory DuckDB database
    db, err := couac.NewDuck()
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Create a connection
    conn, err := db.Connect()
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    ctx := context.Background()

    // Execute DDL
    conn.Exec(ctx, "CREATE TABLE users (id INTEGER, name VARCHAR)")
    conn.Exec(ctx, "INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')")

    // Query returns a QueryResult with a streaming Arrow RecordReader
    res, err := conn.Query(ctx, "SELECT * FROM users ORDER BY id")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Close()

    for res.Reader.Next() {
        rec := res.Reader.RecordBatch()
        for i := 0; i < int(rec.NumRows()); i++ {
            fmt.Printf("id=%s name=%s\n", rec.Column(0).ValueStr(i), rec.Column(1).ValueStr(i))
        }
    }

    // Bulk ingest from an Arrow record batch
    schema := arrow.NewSchema([]arrow.Field{
        {Name: "id", Type: arrow.PrimitiveTypes.Int64},
        {Name: "tag", Type: arrow.BinaryTypes.String},
    }, nil)
    bldr := array.NewRecordBuilder(memory.DefaultAllocator, schema)
    defer bldr.Release()
    bldr.Field(0).(*array.Int64Builder).AppendValues([]int64{10, 20}, nil)
    bldr.Field(1).(*array.StringBuilder).AppendValues([]string{"a", "b"}, nil)
    rec := bldr.NewRecordBatch()
    defer rec.Release()

    n, err := conn.Ingest(ctx, "tags", rec)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ingested %d rows\n", n)

    // Explore metadata with typed navigation
    objs, _ := conn.Objects(ctx)
    catalog, schema_name, table, found := objs.FindTable("tags")
    if found {
        fmt.Printf("found: %s.%s.%s (%d columns)\n",
            catalog, schema_name, table.TableName, len(table.TableColumns))
    }
}

See the pkg.go.dev examples for more usage patterns.

Driver discovery

Couac supports three modes for locating the DuckDB shared library:

1. By name (default)
// Uses "duckdb" — the ADBC driver manager resolves it
// via TOML manifests installed by `dbc install duckdb`.
db, err := couac.NewDuck()

// Or explicitly:
db, err := couac.NewDuck(couac.WithDriverName("duckdb"))
2. By explicit path
db, err := couac.NewDuck(
    couac.WithDriverPath("/usr/local/lib/libduckdb.so"),
)
3. Programmatic manifest lookup
// Searches ADBC driver manifest directories (Env → User → System)
// using the github.com/columnar-tech/dbc/config package.
db, err := couac.NewDuck(couac.WithDriverLookup())

Concurrency model

DuckDB uses MVCC with optimistic concurrency control. Within a single process:

  • Appends never conflict, even on the same table.
  • Concurrent updates/deletes on different rows succeed.
  • Concurrent updates/deletes on the same row produce a conflict error.
  • Only one process may open a database file for writing at a time.

Couac protects its internal connection list with a sync.RWMutex:

Operation Lock Concurrency
Queries, ingests, DDL Read lock Fully concurrent with each other
Compact, ForceCheckpoint, Close Write lock Blocks until all in-flight operations complete; prevents new operations

Open connections remain valid after Compact completes — they are not closed or invalidated.

Safe compaction

DuckDB does not automatically reclaim disk space from deleted or updated rows (VACUUM is a no-op). Couac's Compact method safely reclaims space:

err := db.Compact(ctx) // acquires write lock, pauses all operations

For file-backed databases, Compact:

  1. Runs FORCE CHECKPOINT to flush the WAL
  2. ATTACHes a temporary database file
  3. Runs COPY FROM DATABASE to create a compacted copy
  4. DETACHes and replaces the original file
  5. Runs a final FORCE CHECKPOINT

For in-memory databases, only FORCE CHECKPOINT is run.

database/sql integration

Couac provides a StdDB() method that returns a standard *sql.DB backed by the same underlying DuckDB instance. This is useful for integrating with ORMs, migration tools, or any code that expects a *sql.DB:

db, _ := couac.NewDuck()
defer db.Close()

stdDB := db.StdDB()
defer stdDB.Close()

var count int
stdDB.QueryRowContext(ctx, "SELECT count(*) FROM users").Scan(&count)
Parameterized queries

The database/sql bridge supports parameterized queries with both ? and $N placeholder styles. This is the recommended way to pass user input to queries — it prevents SQL injection and lets DuckDB optimize the query plan.

// ? placeholders
var name string
stdDB.QueryRowContext(ctx,
    "SELECT name FROM users WHERE id = ?", int64(42),
).Scan(&name)

// $N placeholders
var count int
stdDB.QueryRowContext(ctx,
    "SELECT count(*) FROM events WHERE ts > $1 AND category = $2",
    time.Now().Add(-24*time.Hour), "login",
).Scan(&count)

Supported Go parameter types:

Go type Arrow type DuckDB type
int64 Int64 BIGINT
float64 Float64 DOUBLE
bool Boolean BOOLEAN
string String VARCHAR
[]byte Binary BLOB
time.Time Timestamp(µs, UTC) TIMESTAMP
couac.Decimal Decimal128 DECIMAL
nil Null NULL

couac.NullDecimal is also accepted — a NullDecimal with Valid=false binds as NULL.

Note: Named parameters ($name) are not supported by DuckDB's ADBC driver. Only positional ? and $N placeholders are supported.

Prepared statements
stmt, err := stdDB.PrepareContext(ctx, "INSERT INTO scores VALUES (?, ?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

for _, p := range players {
    stmt.ExecContext(ctx, p.Name, p.Score)
}
Transactions
tx, err := stdDB.BeginTx(ctx, nil)
if err != nil {
    log.Fatal(err)
}
tx.ExecContext(ctx, "UPDATE account SET balance = balance - ? WHERE id = ?", int64(100), int64(1))
tx.ExecContext(ctx, "UPDATE account SET balance = balance + ? WHERE id = ?", int64(100), int64(2))
if err := tx.Commit(); err != nil {
    log.Fatal(err)
}

// Read-only transactions
tx, err = stdDB.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})

DuckDB does not support configurable isolation levels; any level other than sql.LevelDefault returns an error.

Native type mapping

Column values are returned as native Go types matching the underlying Arrow column type — no blanket string conversion. The complete mapping:

DuckDB / Arrow type Go type
BOOLEAN bool
TINYINT / SMALLINT / INTEGER / BIGINT int8 / int16 / int32 / int64
UTINYINT / USMALLINT / UINTEGER / UBIGINT uint8 / uint16 / uint32 / uint64
FLOAT float32
DOUBLE float64
VARCHAR string
BLOB []byte
TIMESTAMP / DATE / TIME time.Time
DECIMAL couac.Decimal
INTERVAL / DURATION int64 (duration) or string (interval)
ENUM string
LIST / LARGE_LIST / FIXED_SIZE_LIST couac.List
STRUCT couac.Struct
MAP couac.Map
UNION string (JSON)
NULL nil
Decimal support

The Decimal type provides arbitrary-precision decimal values with lossless BigFloat(), lossy Float64(), and formatted String() conversions. It implements sql.Scanner and driver.Valuer for round-trip support.

NullDecimal follows the same pattern as sql.NullString and sql.NullInt64 for nullable decimal columns:

// Non-nullable decimal
var d couac.Decimal
row.Scan(&d)
precise := d.BigFloat() // lossless *big.Float
approx  := d.Float64()  // lossy float64
text    := d.String()   // "123.45"

// Nullable decimal
var nd couac.NullDecimal
row.Scan(&nd)
if nd.Valid {
    fmt.Println(nd.Decimal.BigFloat())
}
List, Struct, and Map support

DuckDB's nested column types (LIST, STRUCT, MAP) are returned as rich Go types with recursive conversion — no JSON parsing required.

List wraps []any and represents LIST, LARGE_LIST, and FIXED_SIZE_LIST columns. Elements are native Go types (int32, string, nested List/Struct/Map, etc.):

var l couac.List
row.Scan(&l)
fmt.Println(l.Values)    // []any{1, 2, 3}
ints := l.Ints()         // []int64{1, 2, 3}
floats := l.Floats()     // []float64{1.0, 2.0, 3.0}
strs := l.Strings()      // []string{"1", "2", "3"}
bools := l.Bools()       // []bool{true, false}
jsonL := l.JSON()        // List with all elements as JSON strings
j, _ := l.MarshalJSON()  // [1,2,3]

Struct wraps map[string]any and represents STRUCT columns:

var s couac.Struct
row.Scan(&s)
name, ok := s.Get("name")  // convenience accessor
fmt.Println(s.Fields)       // map[string]any{"name": "Alice", "age": int32(30)}
jsonS := s.JSON()            // Struct with all values as JSON strings

Map wraps map[string]any and represents MAP columns:

var m couac.Map
row.Scan(&m)
val, ok := m.Get("key1")   // convenience accessor
keys := m.Keys()            // []string{"key1", "key2"}
jsonM := m.JSON()            // Map with all values as JSON strings

All three types implement sql.Scanner, driver.Valuer, and json.Marshaler. Nullable variants NullList, NullStruct, and NullMap follow the sql.NullString pattern:

var nl couac.NullList
row.Scan(&nl)
if nl.Valid {
    fmt.Println(nl.List.Ints())
}

Nested types can be passed as query parameters — they are JSON-serialized for binding:

l := couac.List{Values: []any{int64(1), int64(2), int64(3)}}
stdDB.QueryRowContext(ctx, "SELECT ?::INTEGER[]", l)
Deep nesting (recursive conversion)

All nested types are recursively converted to Go-native types at any depth — no intermediate JSON step. Inner LIST columns become []any, inner STRUCT columns become map[string]any, and inner MAP columns become map[string]any. NULL values at any nesting level are nil.

Navigate and extract with fully chainable methods — no manual Scan calls, no type assertions, no intermediate variables:

Method Purpose
s.Struct("f") / s.List("f") / s.Map("f") Navigate from Struct to nested child
m.Struct("k") / m.List("k") / m.Map("k") Navigate from Map to nested child
l.StructAt(i) / l.ListAt(i) / l.MapAt(i) Navigate from List to nested child
s.Str("f") / s.Int32("f") / s.Int64("f") / s.Float64("f") / s.Bool("f") Typed leaf from Struct
m.Str("k") / m.Int32("k") / m.Int64("k") / m.Float64("k") / m.Bool("k") Typed leaf from Map
l.StringAt(i) / l.Int32At(i) / l.Int64At(i) / l.Float64At(i) / l.BoolAt(i) Typed leaf from List

Navigation methods return a zero value on failure (missing key, NULL, wrong type). Zero values are safe to chain on — further calls simply return more zeros. Leaf accessors return (T, bool) where bool is false for any failure in the chain.

Nested lists (INTEGER[][]):

var outer couac.List
stdDB.QueryRowContext(ctx, "SELECT [[1, 2], [3, 4, 5]]::INTEGER[][]").Scan(&outer)

fmt.Println(outer.ListAt(0).Ints()) // []int64{1, 2}

Struct containing struct:

var s couac.Struct
stdDB.QueryRowContext(ctx,
    "SELECT {'name': 'Alice', 'address': {'city': 'Montreal', 'zip': '12345'}}::"+
        "STRUCT(name VARCHAR, address STRUCT(city VARCHAR, zip VARCHAR))",
).Scan(&s)

name, _ := s.Str("name")                     // "Alice"
city, _ := s.Struct("address").Str("city")    // "Montreal"

List → Struct → List (depth-3 chain):

var outer couac.List
stdDB.QueryRowContext(ctx,
    "SELECT [{'name': 'a', 'vals': [1, 2]}]::STRUCT(name VARCHAR, vals INTEGER[])[]",
).Scan(&outer)

name, _ := outer.StructAt(0).Str("name")      // "a"
fmt.Println(outer.StructAt(0).List("vals").Ints()) // []int64{1, 2}

Map with struct values:

var m couac.Map
stdDB.QueryRowContext(ctx,
    "SELECT MAP {'alice': {'age': 30}}::MAP(VARCHAR, STRUCT(age INTEGER))",
).Scan(&m)

age, _ := m.Struct("alice").Int32("age")  // int32(30)

NULL values — chaining through NULL safely yields (zero, false):

// NULL element in a list
var l couac.List
stdDB.QueryRowContext(ctx, "SELECT [1, NULL, 3]::INTEGER[]").Scan(&l)
_, ok := l.Int32At(1) // ok == false (NULL)

// NULL struct field
var s couac.Struct
stdDB.QueryRowContext(ctx,
    "SELECT {'name': 'Alice', 'score': NULL}::STRUCT(name VARCHAR, score INTEGER)",
).Scan(&s)
_, ok = s.Int32("score") // ok == false (NULL)

// NULL inner list — chaining through yields empty
var outer couac.List
stdDB.QueryRowContext(ctx, "SELECT [[1], NULL, [3]]::INTEGER[][]").Scan(&outer)
fmt.Println(outer.ListAt(1).Ints()) // [] (empty — NULL inner list)
rows, _ := stdDB.QueryContext(ctx, "SELECT id, price FROM products")
types, _ := rows.ColumnTypes()
for _, ct := range types {
    fmt.Printf("%s: %s (scan: %v)\n",
        ct.Name(),
        ct.DatabaseTypeName(), // "INTEGER", "DECIMAL", "VARCHAR", etc.
        ct.ScanType(),         // reflect.Type of the Go value
    )
    // DecimalSize() returns precision/scale for DECIMAL columns
    if p, s, ok := ct.DecimalSize(); ok {
        fmt.Printf("  precision=%d scale=%d\n", p, s)
    }
    // Nullable() reports whether the column allows NULLs
    if nullable, ok := ct.Nullable(); ok {
        fmt.Printf("  nullable=%v\n", nullable)
    }
}
Implemented driver interfaces

The database/sql bridge implements the following database/sql/driver interfaces for full integration with the standard library:

Interface Type Purpose
driver.Connector dbConnector Connection factory for sql.OpenDB
driver.Conn sqlConn Base connection
driver.ConnPrepareContext sqlConn Context-aware Prepare
driver.ConnBeginTx sqlConn Context + TxOptions for BeginTx
driver.ExecerContext sqlConn One-shot exec (no Prepare round-trip)
driver.QueryerContext sqlConn One-shot query (no Prepare round-trip)
driver.NamedValueChecker sqlConn Accept Decimal/NullDecimal/List/Struct/Map params
driver.Stmt sqlStmt Prepared statement
driver.StmtExecContext sqlStmt Parameterized exec with context
driver.StmtQueryContext sqlStmt Parameterized query with context
driver.Tx sqlTx Transaction commit/rollback
driver.Rows sqlRows Row iteration over Arrow batches
driver.RowsColumnTypeScanType sqlRows Go reflect.Type for each column
driver.RowsColumnTypeDatabaseTypeName sqlRows DuckDB type name per column
driver.RowsColumnTypeNullable sqlRows Nullability per column
driver.RowsColumnTypePrecisionScale sqlRows Precision/scale for DECIMAL

DuckDB ADBC limitations

DuckDB's ADBC driver supports the full ADBC specification with these exceptions:

Feature Status Couac workaround
IngestMode.CreateAppend ❌ Not supported Ingest probes with GetTableSchema, switches between Create/Append
Statement-level AutoCommit ❌ Not supported WithTransaction uses explicit BEGIN/COMMIT/ROLLBACK SQL
ReadPartition / ExecutePartitions ❌ N/A Not applicable to a non-distributed database
VACUUM No-op Compact uses COPY FROM DATABASE for true space reclamation

Migration guide

If upgrading from an older version of couac, note the following breaking changes:

Catalog() / DBSchema() return string instead of *string
// Old
catalog := conn.Catalog()  // *string
schema  := conn.DBSchema() // *string

// New
catalog := conn.Catalog()  // string
schema  := conn.DBSchema() // string
Type renames (with go:fix inline migration)

The primary types have been renamed for clarity. The old names remain as type aliases with //go:fix inline directives, so running go fix ./... (Go 1.26+) will automatically update your code:

// Old → New (automatically fixable with go fix)
couac.Quacker    → couac.DB
couac.QuackCon   → couac.Conn
couac.DBObject   → couac.CatalogInfo
couac.DBSchema   → couac.SchemaInfo  // the type; method DBSchema() is unchanged
couac.DBObjects  → couac.CatalogTree
Method renames

Connection and query methods have been renamed. Deprecated wrappers are provided for the old names:

// Old → New
db.NewConnection()                      → db.Connect()
db.NewConnectionWithOpts(cat, sch)      → db.ConnectAs(cat, sch)
conn.IngestCreateAppend(ctx, tbl, rec)  → conn.Ingest(ctx, tbl, rec)
conn.IngestCreateAppendMerge(...)       → conn.IngestMerge(...)
conn.GetObjects(ctx, opts...)           → conn.Objects(ctx, opts...)
conn.GetObjectsMap(ctx)                 → conn.ObjectsMap(ctx)
conn.GetTableSchema(ctx, &c, &s, tbl)  → conn.TableSchema(ctx, tbl)
                                        → conn.TableSchemaIn(ctx, c, s, tbl)
conn.GetTableTypes(ctx)                 → conn.TableTypes(ctx)
conn.GetSetting(ctx, key)              → conn.Setting(ctx, key)
GetTableSchema replaced with TableSchema / TableSchemaIn

The *string parameters have been removed:

// Old
s, err := conn.GetTableSchema(ctx, &catalog, &schema, "table")

// New — uses connection defaults
s, err := conn.TableSchema(ctx, "table")

// New — explicit catalog/schema (plain strings, empty = default)
s, err := conn.TableSchemaIn(ctx, "memory", "main", "table")
Query returns *QueryResult instead of 4 values
// Old
rr, stmt, n, err := conn.Query(ctx, "SELECT 1")
defer rr.Release()
defer stmt.Close()

// New
res, err := conn.Query(ctx, "SELECT 1")
defer res.Close()
rr := res.Reader          // array.RecordReader
n  := res.RowsAffected    // int64

// If you need the original 4-return form:
rr, stmt, n, err := conn.QueryRaw(ctx, "SELECT 1")
Constructor signature change
// Old
db, err := couac.NewDB("path.db")

// New
db, err := couac.NewDuck(couac.WithPath("path.db"))

Type aliases

For backward compatibility, the following type aliases are provided with //go:fix inline directives (Go 1.26+):

type Quacker      = DB          // go:fix inline
type QuackCon     = Conn        // go:fix inline
type DuckDatabase = DB          // go:fix inline
type Connection   = Conn        // go:fix inline
type DBObject     = CatalogInfo // go:fix inline
type DBSchema     = SchemaInfo  // go:fix inline
type DBObjects    = CatalogTree // go:fix inline
type Statement    = adbc.Statement

💫 Show your support

Give a ⭐️ if this project helped you! Feedback and PRs welcome.

License

Couac is released under the Apache 2.0 license. See LICENCE.txt

Documentation

Overview

Package couac provides a convenient, ergonomic wrapper around ADBC (Arrow Database Connectivity) for DuckDB.

Couac simplifies working with DuckDB through ADBC by providing:

  • Database lifecycle management with automatic driver discovery
  • Connection pooling with concurrency-safe tracking
  • Query execution returning Apache Arrow record batches
  • Bulk ingestion from Arrow record batches with automatic table creation
  • Schema evolution via UNION BY NAME merge strategy
  • Hierarchical catalog/schema/table metadata with typed navigation
  • Safe database compaction without disrupting open connections
  • Extension and secret management
  • Pragma, tuning, and introspection convenience wrappers

DuckDB's ADBC driver supports the full ADBC specification except for ReadPartition and ExecutePartitions (not applicable to a non-distributed database). Notably, DuckDB does not support the CreateAppend ingest mode or statement-level AutoCommit; couac works around both limitations.

Concurrency Model

DuckDB uses MVCC with optimistic concurrency control. Within a single process, multiple connections may read and write concurrently:

  • Appends never conflict, even on the same table.
  • Concurrent updates/deletes on different rows succeed.
  • Concurrent updates/deletes on the same row produce a conflict error.
  • Only one process may open a database file for writing at a time.

Couac protects its internal connection list with a sync.RWMutex. Normal operations acquire a read lock (allowing full concurrency). Maintenance operations like DB.Compact acquire a write lock, blocking new operations until maintenance completes.

Driver Discovery

Couac supports three modes for locating the DuckDB shared library:

If no driver option is supplied, couac defaults to WithDriverName("duckdb").

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilRecord is returned when a nil arrow.RecordBatch is passed to an ingest function.
	ErrNilRecord = errors.New("couac: nil arrow record batch")
	// ErrEmptyTable is returned when an empty destination table name is provided.
	ErrEmptyTable = errors.New("couac: empty destination table name")
	// ErrDatabaseClosed is returned when an operation is attempted on a closed database.
	ErrDatabaseClosed = errors.New("couac: database is closed")
	// ErrConnectionClosed is returned when an operation is attempted on a closed connection.
	ErrConnectionClosed = errors.New("couac: connection is closed")
	// ErrDriverNotFound is returned when the DuckDB driver cannot be located.
	ErrDriverNotFound = errors.New("couac: duckdb driver not found; install with: dbc install duckdb")
	// ErrPathAlreadyOpen is returned when attempting to open a database file that
	// is already open in this process.
	ErrPathAlreadyOpen = errors.New("couac: database file is already open in this process")
)

Sentinel errors returned by couac functions.

Functions

func Elem added in v1.1.0

func Elem[T any](l List, index int) (T, bool)

Elem returns the element at index i from a List as type T. If the index is out of range, the element is nil, or it cannot be converted to T, the zero value and false are returned. The same Scan-fallback logic as Field applies for nested types.

Example:

n, ok := couac.Elem[int32](l, 0)
inner, ok := couac.Elem[couac.Struct](l, 0) // auto-Scans

func ExtensionsDir added in v1.0.0

func ExtensionsDir() string

ExtensionsDir returns the default directory where DuckDB stores installed extension binaries (~/.duckdb/extensions/).

func Field added in v1.1.0

func Field[T any](s Struct, name string) (T, bool)

Field returns the value of a Struct field as type T. If the field does not exist, is nil, or cannot be converted to T, the zero value and false are returned.

For leaf types (int32, string, bool, etc.) a direct type assertion is used. For nested types (List, Struct, Map) the raw value ([]any or map[string]any) is automatically passed through Scan, so the full wrapper API is available on the result.

Example:

name, ok := couac.Field[string](s, "name")
age, ok  := couac.Field[int32](s, "age")
addr, ok := couac.Field[couac.Struct](s, "address") // auto-Scans map[string]any
tags, ok := couac.Field[couac.List](s, "tags")       // auto-Scans []any

func SecretsDir added in v1.0.0

func SecretsDir() string

SecretsDir returns the default directory where DuckDB stores persistent secrets (~/.duckdb/stored_secrets/).

func Value added in v1.1.0

func Value[T any](m Map, key string) (T, bool)

Value returns the value for a Map key as type T. If the key does not exist, is nil, or cannot be converted to T, the zero value and false are returned. The same Scan-fallback logic as Field applies for nested types.

Example:

count, ok := couac.Value[int32](m, "key1")
inner, ok := couac.Value[couac.Struct](m, "alice") // auto-Scans

Types

type AttachOption added in v1.0.0

type AttachOption func(*attachConfig)

AttachOption configures a Conn.Attach call.

func ReadOnly added in v1.0.0

func ReadOnly() AttachOption

ReadOnly configures an ATTACH operation to open the database in read-only mode.

func WithBlockSize added in v1.0.0

func WithBlockSize(size int) AttachOption

WithBlockSize sets the block size for an attached database. Must be a power of 2 between 16384 (16 KB) and 262144 (256 KB).

func WithEncryptionKey added in v1.0.0

func WithEncryptionKey(key string) AttachOption

WithEncryptionKey sets the encryption key for an attached database.

type CatalogInfo added in v1.0.0

type CatalogInfo struct {
	CatalogName      string       `json:"catalog_name"`
	CatalogDBSchemas []SchemaInfo `json:"catalog_db_schemas"`
}

CatalogInfo represents a single catalog in the DuckDB database hierarchy. When multiple databases are ATTACHed, each appears as a separate CatalogInfo.

type CatalogTree added in v1.0.0

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

CatalogTree wraps a []CatalogInfo and provides typed navigation methods for exploring catalog metadata without additional ADBC round-trips.

Use Conn.Objects to obtain a CatalogTree instance.

func (*CatalogTree) Catalogs added in v1.0.0

func (o *CatalogTree) Catalogs() []string

Catalogs returns the names of all catalogs in the result set.

func (*CatalogTree) Columns added in v1.0.0

func (o *CatalogTree) Columns(catalog, schema, table string) []ColumnSchema

Columns returns the columns for the specified table. Returns nil if the catalog, schema, or table is not found.

func (*CatalogTree) Constraints added in v1.0.0

func (o *CatalogTree) Constraints(catalog, schema, table string) []ConstraintSchema

Constraints returns the constraints for the specified table. Returns nil if the catalog, schema, or table is not found.

func (*CatalogTree) FindTable added in v1.0.0

func (o *CatalogTree) FindTable(name string) (catalog, schema string, table *TableSchema, found bool)

FindTable searches all catalogs and schemas for a table with the given name. Returns the catalog, schema, table, and whether it was found. If multiple tables share the same name across catalogs/schemas, the first match is returned.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE orders (id INTEGER, total DECIMAL(10,2))")

	objs, err := conn.Objects(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	catalog, schema, table, found := objs.FindTable("orders")
	if found {
		fmt.Printf("found: %s.%s.%s (%d columns)\n",
			catalog, schema, table.TableName, len(table.TableColumns))
	}
}
Output:
found: memory.main.orders (2 columns)

func (*CatalogTree) Map added in v1.0.0

func (o *CatalogTree) Map() map[string]map[string][]TableSchema

Map returns the catalog hierarchy as a nested map: catalog name → schema name → []TableSchema. This is useful for bulk iteration over all metadata.

func (*CatalogTree) Raw added in v1.0.0

func (o *CatalogTree) Raw() []CatalogInfo

Raw returns the underlying []CatalogInfo slice for direct access.

func (*CatalogTree) Schemas added in v1.0.0

func (o *CatalogTree) Schemas(catalog string) []string

Schemas returns the schema names within the given catalog. Returns nil if the catalog is not found.

func (*CatalogTree) TableExists added in v1.0.0

func (o *CatalogTree) TableExists(catalog, schema, table string) bool

TableExists reports whether a table with the given name exists in the specified catalog and schema.

func (*CatalogTree) Tables added in v1.0.0

func (o *CatalogTree) Tables(catalog, schema string) []TableSchema

Tables returns the tables within the given catalog and schema. Returns nil if the catalog or schema is not found.

type ColumnInfo added in v1.0.0

type ColumnInfo struct {
	Name    string `json:"column_name"`
	Type    string `json:"column_type"`
	Null    string `json:"null"`
	Key     string `json:"key"`
	Default string `json:"default"`
	Extra   string `json:"extra"`
}

ColumnInfo describes a column as returned by DESCRIBE.

type ColumnSchema added in v0.5.3

type ColumnSchema struct {
	ColumnName            string `json:"column_name"`
	OrdinalPosition       int32  `json:"ordinal_position"`
	Remarks               string `json:"remarks"`
	XdbcDataType          int16  `json:"xdbc_data_type"`
	XdbcTypeName          string `json:"xdbc_type_name"`
	XdbcColumnSize        int32  `json:"xdbc_column_size"`
	XdbcDecimalDigits     int16  `json:"xdbc_decimal_digits"`
	XdbcNumPrecRadix      int16  `json:"xdbc_num_prec_radix"`
	XdbcNullable          int16  `json:"xdbc_nullable"`
	XdbcColumnDef         string `json:"xdbc_column_def"`
	XdbcSqlDataType       int16  `json:"xdbc_sql_data_type"`
	XdbcDatetimeSub       int16  `json:"xdbc_datetime_sub"`
	XdbcCharOctetLength   int32  `json:"xdbc_char_octet_length"`
	XdbcIsNullable        string `json:"xdbc_is_nullable"`
	XdbcScopeCatalog      string `json:"xdbc_scope_catalog"`
	XdbcScopeSchema       string `json:"xdbc_scope_schema"`
	XdbcScopeTable        string `json:"xdbc_scope_table"`
	XdbcIsAutoincrement   bool   `json:"xdbc_is_autoincrement"`
	XdbcIsGeneratedColumn bool   `json:"xdbc_is_generatedcolumn"`
}

ColumnSchema describes a single column within a table.

type Conn added in v1.0.0

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

Conn represents a single connection to a DuckDB database.

Conn wraps an ADBC connection and provides methods for executing queries, ingesting data, and inspecting metadata. Each Conn is tracked by its parent DB and will be closed automatically when the parent is closed.

Conn methods that perform database operations acquire a read lock on the parent DB's mutex, allowing concurrent use. Maintenance operations on the parent (e.g. DB.Compact) will block until all in-flight operations complete.

func (*Conn) Attach added in v1.0.0

func (q *Conn) Attach(ctx context.Context, path, alias string, opts ...AttachOption) error

Attach attaches an additional database file to the current DuckDB instance under the given alias. The attached database appears as a separate catalog and can be queried with fully-qualified names (e.g. alias.schema.table).

Attachment definitions are NOT persisted between DuckDB sessions; you must re-attach on each NewDuck call.

Options:

Example:

err := conn.Attach(ctx, "other.db", "other_db", couac.ReadOnly())
res, _ := conn.Query(ctx, "SELECT * FROM other_db.main.users")

func (*Conn) Catalog added in v1.0.0

func (q *Conn) Catalog() string

Catalog returns the connection's catalog name. An empty string indicates the default catalog.

func (*Conn) Close added in v1.0.0

func (q *Conn) Close() error

Close closes this connection and removes it from the parent's connection tracking. Close is idempotent; calling it more than once returns nil.

It is important to close connections to allow DuckDB to properly commit WAL file changes.

func (*Conn) CopyDatabase added in v1.0.0

func (q *Conn) CopyDatabase(ctx context.Context, srcAlias, dstPath string) error

CopyDatabase copies all data from the source database (identified by its catalog alias) to a new database file at dstPath. This creates a perfectly compacted copy.

Example:

conn.CopyDatabase(ctx, "memory", "/tmp/backup.db")

func (*Conn) DBSchema added in v1.0.0

func (q *Conn) DBSchema() string

DBSchema returns the connection's database schema. An empty string indicates the default schema ("main").

func (*Conn) DatabaseSize added in v1.0.0

func (q *Conn) DatabaseSize(ctx context.Context) ([]DatabaseSize, error)

DatabaseSize returns size information for all databases (including attached databases).

func (*Conn) Databases added in v1.0.0

func (q *Conn) Databases(ctx context.Context) ([]string, error)

Databases returns the list of all attached database names (catalogs), including the default database.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	dbs, err := conn.Databases(context.Background())
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// In-memory DuckDB always has at least the "memory" catalog.
	fmt.Println("has databases:", len(dbs) > 0)
}
Output:
has databases: true

func (*Conn) Describe added in v1.0.0

func (q *Conn) Describe(ctx context.Context, tableOrQuery string) ([]ColumnInfo, error)

Describe returns column information for a table or query result. Pass either a table name or a full SELECT statement.

Example:

cols, err := conn.Describe(ctx, "users")
cols, err := conn.Describe(ctx, "SELECT * FROM users WHERE age > 21")
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE metrics (ts TIMESTAMP, value DOUBLE, tag VARCHAR)")

	cols, err := conn.Describe(ctx, "metrics")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	for _, c := range cols {
		fmt.Printf("%s: %s\n", c.Name, c.Type)
	}
}
Output:
ts: TIMESTAMP
value: DOUBLE
tag: VARCHAR

func (*Conn) Detach added in v1.0.0

func (q *Conn) Detach(ctx context.Context, alias string) error

Detach detaches a previously attached database. The alias must match the one used in Conn.Attach.

func (*Conn) DisableProfiling added in v1.0.0

func (q *Conn) DisableProfiling(ctx context.Context) error

DisableProfiling disables query profiling.

func (*Conn) EnableProfiling added in v1.0.0

func (q *Conn) EnableProfiling(ctx context.Context, format string) error

EnableProfiling enables query profiling. Format can be "json", "query_tree", "query_tree_optimizer", or "no_output".

func (*Conn) Exec added in v1.0.0

func (q *Conn) Exec(ctx context.Context, query string) (int64, error)

Exec executes a statement that does not generate a result set (DDL, DML). It returns the number of rows affected if known, otherwise -1.

Exec acquires a read lock on the parent database, allowing concurrent execution with other operations but blocking during maintenance.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	// Create a table and insert data.
	_, err = conn.Exec(context.Background(), "CREATE TABLE greetings (msg VARCHAR)")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	n, err := conn.Exec(context.Background(), "INSERT INTO greetings VALUES ('hello'), ('world')")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("rows inserted:", n)
}
Output:
rows inserted: 2

func (*Conn) Explain added in v1.0.0

func (q *Conn) Explain(ctx context.Context, query string) (string, error)

Explain returns the query plan for a SQL statement without executing it.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE t (x INT)")

	plan, err := conn.Explain(ctx, "SELECT * FROM t WHERE x > 5")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// The plan contains a scan operator
	fmt.Println("has plan:", plan != "")
}
Output:
has plan: true

func (*Conn) Extensions added in v1.0.0

func (q *Conn) Extensions(ctx context.Context) ([]Extension, error)

Extensions returns the list of DuckDB extensions with their status.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	exts, err := conn.Extensions(context.Background())
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// DuckDB always has at least a few built-in extensions
	fmt.Println("has extensions:", len(exts) > 0)
}
Output:
has extensions: true

func (*Conn) GetObjects deprecated added in v1.0.0

func (q *Conn) GetObjects(ctx context.Context, opts ...ObjectsOption) (*CatalogTree, error)

GetObjects is a backward-compatible alias for Conn.Objects.

Deprecated: Use Conn.Objects instead.

func (*Conn) GetObjectsMap deprecated added in v1.0.0

func (q *Conn) GetObjectsMap(ctx context.Context) (map[string]map[string][]TableSchema, error)

GetObjectsMap is a backward-compatible alias for Conn.ObjectsMap.

Deprecated: Use Conn.ObjectsMap instead.

func (*Conn) GetSetting deprecated added in v1.0.0

func (q *Conn) GetSetting(ctx context.Context, key string) (string, error)

GetSetting is a backward-compatible alias for Conn.Setting.

Deprecated: Use Conn.Setting instead.

func (*Conn) GetTableSchema deprecated added in v1.0.0

func (q *Conn) GetTableSchema(ctx context.Context, catalog, dbSchema *string, tableName string) (*arrow.Schema, error)

GetTableSchema is a backward-compatible alias for Conn.TableSchemaIn. It accepts *string parameters for catalog and dbSchema.

Deprecated: Use Conn.TableSchema or Conn.TableSchemaIn instead.

func (*Conn) GetTableTypes deprecated added in v1.0.0

func (q *Conn) GetTableTypes(ctx context.Context) ([]string, error)

GetTableTypes is a backward-compatible alias for Conn.TableTypes.

Deprecated: Use Conn.TableTypes instead.

func (*Conn) Ingest added in v1.0.0

func (q *Conn) Ingest(ctx context.Context, destTable string, rec arrow.RecordBatch) (int64, error)

Ingest ingests an Arrow record batch into the DuckDB database, creating the table from the record's schema if it does not exist, or appending if it does.

DuckDB does not support ADBC's CreateAppend ingest mode, so this method probes for the table via GetTableSchema and switches between Create and Append modes accordingly.

It returns the number of rows affected if known, otherwise -1.

The connection's Catalog and DBSchema are used as the target catalog and schema if set.

Example
package main

import (
	"context"
	"fmt"

	"github.com/apache/arrow-go/v18/arrow"
	"github.com/apache/arrow-go/v18/arrow/array"
	"github.com/apache/arrow-go/v18/arrow/memory"
	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	// Build an Arrow record batch.
	schema := arrow.NewSchema([]arrow.Field{
		{Name: "id", Type: arrow.PrimitiveTypes.Int64},
		{Name: "name", Type: arrow.BinaryTypes.String},
	}, nil)
	bldr := array.NewRecordBuilder(memory.DefaultAllocator, schema)
	defer bldr.Release()
	bldr.Field(0).(*array.Int64Builder).AppendValues([]int64{1, 2, 3}, nil)
	bldr.Field(1).(*array.StringBuilder).AppendValues([]string{"Alice", "Bob", "Charlie"}, nil)
	rec := bldr.NewRecordBatch()
	defer rec.Release()

	// Ingest creates the table on first call, appends on subsequent calls.
	ctx := context.Background()
	n, err := conn.Ingest(ctx, "users", rec)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("ingested:", n, "rows")

	// Verify.
	res, err := conn.Query(ctx, "SELECT count(*) FROM users")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer res.Close()
	if res.Reader.Next() {
		fmt.Println("total:", res.Reader.RecordBatch().Column(0).ValueStr(0))
	}
}
Output:
ingested: 3 rows
total: 3

func (*Conn) IngestCreateAppend deprecated added in v1.0.0

func (q *Conn) IngestCreateAppend(ctx context.Context, destTable string, rec arrow.RecordBatch) (int64, error)

IngestCreateAppend is a backward-compatible alias for Conn.Ingest.

Deprecated: Use Conn.Ingest instead.

func (*Conn) IngestCreateAppendMerge deprecated added in v1.0.0

func (q *Conn) IngestCreateAppendMerge(ctx context.Context, destTable string, rec arrow.RecordBatch) (int64, error)

IngestCreateAppendMerge is a backward-compatible alias for Conn.IngestMerge.

Deprecated: Use Conn.IngestMerge instead.

func (*Conn) IngestMerge added in v1.0.0

func (q *Conn) IngestMerge(ctx context.Context, destTable string, rec arrow.RecordBatch) (int64, error)

IngestMerge ingests an Arrow record batch with automatic schema evolution. If the target table does not exist, it is created. If the table exists and the record's schema matches, data is appended. If the schemas differ, the record is ingested into a temporary merge table and then merged into the target using UNION BY NAME, which adds any new columns.

It returns the number of rows affected if known, otherwise -1.

This is useful when the schema of incoming data may evolve over time (e.g. new fields added to a protobuf message).

Example
package main

import (
	"context"
	"fmt"

	"github.com/apache/arrow-go/v18/arrow"
	"github.com/apache/arrow-go/v18/arrow/array"
	"github.com/apache/arrow-go/v18/arrow/memory"
	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()

	// First ingest: creates table with {id, name}.
	schema1 := arrow.NewSchema([]arrow.Field{
		{Name: "id", Type: arrow.PrimitiveTypes.Int64},
		{Name: "name", Type: arrow.BinaryTypes.String},
	}, nil)
	bldr1 := array.NewRecordBuilder(memory.DefaultAllocator, schema1)
	defer bldr1.Release()
	bldr1.Field(0).(*array.Int64Builder).Append(1)
	bldr1.Field(1).(*array.StringBuilder).Append("Alice")
	rec1 := bldr1.NewRecordBatch()
	defer rec1.Release()

	conn.Ingest(ctx, "evolving", rec1)

	// Second ingest: adds a new "email" column via UNION BY NAME.
	schema2 := arrow.NewSchema([]arrow.Field{
		{Name: "id", Type: arrow.PrimitiveTypes.Int64},
		{Name: "name", Type: arrow.BinaryTypes.String},
		{Name: "email", Type: arrow.BinaryTypes.String},
	}, nil)
	bldr2 := array.NewRecordBuilder(memory.DefaultAllocator, schema2)
	defer bldr2.Release()
	bldr2.Field(0).(*array.Int64Builder).Append(2)
	bldr2.Field(1).(*array.StringBuilder).Append("Bob")
	bldr2.Field(2).(*array.StringBuilder).Append("bob@example.com")
	rec2 := bldr2.NewRecordBatch()
	defer rec2.Release()

	n, err := conn.IngestMerge(ctx, "evolving", rec2)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("merged:", n, "rows")

	// The table now has 3 columns and 2 rows.
	res, _ := conn.Query(ctx, "SELECT * FROM evolving ORDER BY id")
	defer res.Close()
	fmt.Println("schema:", res.Reader.Schema())
}
Output:
merged: 1 rows
schema: schema:
  fields: 3
    - id: type=int64, nullable
    - name: type=utf8, nullable
    - email: type=utf8, nullable

func (*Conn) IngestReplace added in v1.0.0

func (q *Conn) IngestReplace(ctx context.Context, destTable string, rec arrow.RecordBatch) (int64, error)

IngestReplace ingests an Arrow record batch, replacing the target table entirely (DROP + CREATE). It returns the number of rows affected if known, otherwise -1.

This uses ADBC's Replace ingest mode, which is supported since ADBC 1.1.0 / DuckDB 0.9.0+.

func (*Conn) IngestStream added in v1.0.0

func (q *Conn) IngestStream(ctx context.Context, destTable string, reader array.RecordReader) (int64, error)

IngestStream ingests data from an Arrow RecordReader, which provides streaming access to record batches. This is more memory-efficient than [Ingest] for large datasets because it doesn't require holding all data in memory at once.

The table is created if it does not exist, or appended to if it does.

func (*Conn) InstallExtension added in v1.0.0

func (q *Conn) InstallExtension(ctx context.Context, name string) error

InstallExtension installs a DuckDB extension by name. The extension is downloaded from the DuckDB extension repository if not already installed. Use [QuackCon.LoadExtension] to load it after installation.

Example:

conn.InstallExtension(ctx, "httpfs")
conn.LoadExtension(ctx, "httpfs")

func (*Conn) LoadExtension added in v1.0.0

func (q *Conn) LoadExtension(ctx context.Context, name string) error

LoadExtension loads a previously installed DuckDB extension. Extensions cannot be unloaded or reloaded at runtime.

func (*Conn) LockConfiguration added in v1.0.0

func (q *Conn) LockConfiguration(ctx context.Context) error

LockConfiguration prevents further configuration changes for the remainder of this session. This is useful to lock in settings during stable operation.

func (*Conn) NewStatement added in v1.0.0

func (q *Conn) NewStatement() (Statement, error)

NewStatement creates a raw ADBC Statement on this connection. The caller must close the statement when done.

Use this for advanced ADBC operations not covered by the convenience methods. For most use cases, prefer Conn.Exec, Conn.Query, or Conn.Prepare.

func (*Conn) Objects added in v1.0.0

func (q *Conn) Objects(ctx context.Context, opts ...ObjectsOption) (*CatalogTree, error)

Objects retrieves a hierarchical view of database objects (catalogs, schemas, tables, columns) and returns a CatalogTree wrapper with typed navigation methods.

Options control the depth of recursion and filtering:

objs, err := conn.Objects(ctx,
    couac.WithDepth(couac.ObjectDepthTables),
    couac.WithCatalogFilter("memory"),
    couac.WithTableTypes([]string{"TABLE"}),
)
if err != nil { ... }
for _, t := range objs.Tables("memory", "main") {
    fmt.Println(t.TableName)
}

When databases are ATTACHed, each attached database appears as a separate catalog in the returned hierarchy.

The result is an Arrow dataset with the following schema:

Field Name              | Field Type
------------------------|----------------------------
catalog_name            | utf8
catalog_db_schemas      | list<DB_SCHEMA_SCHEMA>

DB_SCHEMA_SCHEMA is a Struct with the fields:

Field Name              | Field Type
------------------------|----------------------------
db_schema_name          | utf8
db_schema_tables        | list<TABLE_SCHEMA>

TABLE_SCHEMA is a Struct with the fields:

Field Name              | Field Type
------------------------|----------------------------
table_name              | utf8 not null
table_type              | utf8 not null
table_columns           | list<COLUMN_SCHEMA>
table_constraints       | list<CONSTRAINT_SCHEMA>

See CatalogInfo, SchemaInfo, TableSchema, ColumnSchema, ConstraintSchema, and UsageSchema for the full field definitions.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE products (id INTEGER, name VARCHAR)")

	objs, err := conn.Objects(ctx, couac.WithDepth(couac.ObjectDepthTables))
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("has catalogs:", len(objs.Catalogs()) > 0)
	for _, t := range objs.Tables("memory", "main") {
		if t.TableName == "products" {
			fmt.Println("found table:", t.TableName, "type:", t.TableType)
		}
	}
}
Output:
has catalogs: true
found table: products type: BASE TABLE

func (*Conn) ObjectsMap added in v1.0.0

func (q *Conn) ObjectsMap(ctx context.Context) (map[string]map[string][]TableSchema, error)

ObjectsMap is a convenience function that retrieves all database objects and returns them as a nested map: catalog name → schema name → []TableSchema.

This is equivalent to calling Objects followed by CatalogTree.Map.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE map_demo (x INT)")

	// ObjectsMap returns a nested catalog→schema→tables map.
	m, err := conn.ObjectsMap(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// Check if the table exists in the map.
	if tables, ok := m["memory"]["main"]; ok {
		for _, t := range tables {
			if t.TableName == "map_demo" {
				fmt.Println("found:", t.TableName, "type:", t.TableType)
			}
		}
	}
}
Output:
found: map_demo type: BASE TABLE

func (*Conn) Platform added in v1.0.0

func (q *Conn) Platform(ctx context.Context) (string, error)

Platform returns the DuckDB platform identifier (e.g. "linux_amd64", "osx_arm64", "windows_amd64").

func (*Conn) Prepare added in v1.0.0

func (q *Conn) Prepare(ctx context.Context, query string) (Statement, error)

Prepare creates a prepared statement for the given SQL query. The caller must close the statement when done.

Use prepared statements for repeatedly executing the same query with different parameters. Bind parameters with [Statement.Bind] before executing.

Example:

stmt, err := conn.Prepare(ctx, "INSERT INTO t VALUES (?, ?)")
if err != nil { ... }
defer stmt.Close()
stmt.Bind(ctx, record)
n, err := stmt.ExecuteUpdate(ctx)
Example
package main

import (
	"context"
	"fmt"

	"github.com/apache/arrow-go/v18/arrow"
	"github.com/apache/arrow-go/v18/arrow/array"
	"github.com/apache/arrow-go/v18/arrow/memory"
	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE kv (key VARCHAR, value INT)")

	// Prepare returns a raw ADBC Statement for Arrow-native parameter binding.
	stmt, err := conn.Prepare(ctx, "INSERT INTO kv VALUES (?, ?)")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer stmt.Close()

	// Build an Arrow record batch with the parameters.
	schema := arrow.NewSchema([]arrow.Field{
		{Name: "key", Type: arrow.BinaryTypes.String},
		{Name: "value", Type: arrow.PrimitiveTypes.Int64},
	}, nil)
	bldr := array.NewRecordBuilder(memory.DefaultAllocator, schema)
	defer bldr.Release()
	bldr.Field(0).(*array.StringBuilder).Append("hello")
	bldr.Field(1).(*array.Int64Builder).Append(42)
	rec := bldr.NewRecordBatch()
	defer rec.Release()

	// Bind the Arrow record batch and execute.
	if err := stmt.Bind(ctx, rec); err != nil {
		fmt.Println("Error:", err)
		return
	}
	n, err := stmt.ExecuteUpdate(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("inserted:", n)
}
Output:
inserted: 1

func (*Conn) Query added in v1.0.0

func (q *Conn) Query(ctx context.Context, query string) (*QueryResult, error)

Query executes a SQL query and returns a QueryResult containing a streaming Arrow array.RecordReader. The caller must call QueryResult.Close when done reading results.

Query acquires a read lock only for the statement execution; the returned RecordReader can be consumed after the lock is released.

Example:

res, err := conn.Query(ctx, "SELECT * FROM users WHERE age > 21")
if err != nil {
    return err
}
defer res.Close()
for res.Reader.Next() {
    rec := res.Reader.RecordBatch()
    // process rec...
}
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE colors (name VARCHAR, hex VARCHAR)")
	conn.Exec(ctx, "INSERT INTO colors VALUES ('red', '#FF0000'), ('green', '#00FF00')")

	// Query returns a QueryResult with a streaming RecordReader.
	res, err := conn.Query(ctx, "SELECT name, hex FROM colors ORDER BY name")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer res.Close()

	for res.Reader.Next() {
		rec := res.Reader.RecordBatch()
		for i := 0; i < int(rec.NumRows()); i++ {
			fmt.Printf("%s = %s\n", rec.Column(0).ValueStr(i), rec.Column(1).ValueStr(i))
		}
	}
}
Output:
green = #00FF00
red = #FF0000

func (*Conn) QueryRaw added in v1.0.0

func (q *Conn) QueryRaw(ctx context.Context, query string) (array.RecordReader, adbc.Statement, int64, error)

QueryRaw executes a SQL query and returns the raw components: a RecordReader, the underlying ADBC Statement, and the number of rows affected. This is the original 4-return-value form for callers that need direct access to the statement.

The caller is responsible for closing both the RecordReader and the Statement. Prefer Conn.Query for simpler resource management.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE raw_demo (x INT)")
	conn.Exec(ctx, "INSERT INTO raw_demo VALUES (10), (20), (30)")

	// QueryRaw returns the reader, statement, and row count separately.
	rr, stmt, n, err := conn.QueryRaw(ctx, "SELECT * FROM raw_demo")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer rr.Release()
	defer stmt.Close()

	fmt.Println("rows affected:", n)
	for rr.Next() {
		rec := rr.RecordBatch()
		fmt.Println("batch rows:", rec.NumRows())
	}
}
Output:
rows affected: -1
batch rows: 3

func (*Conn) Reset added in v1.0.0

func (q *Conn) Reset(ctx context.Context, key string) error

Reset restores a DuckDB configuration option to its default value.

func (*Conn) Secrets added in v1.0.0

func (q *Conn) Secrets(ctx context.Context) ([]Secret, error)

Secrets returns the list of stored DuckDB secrets. Sensitive fields are redacted by DuckDB.

func (*Conn) Set added in v1.0.0

func (q *Conn) Set(ctx context.Context, key, value string) error

Set sets a DuckDB configuration option. Most options are GLOBAL (instance-wide); some are SESSION-scoped (connection-specific). Use Conn.Setting to read the current value, and Conn.Reset to restore the default.

Example:

conn.Set(ctx, "memory_limit", "4GB")
conn.Set(ctx, "threads", "8")
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()

	// Set and read back a configuration value.
	if err := conn.Set(ctx, "threads", "2"); err != nil {
		fmt.Println("Error:", err)
		return
	}
	val, err := conn.Setting(ctx, "threads")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("threads:", val)
}
Output:
threads: 2

func (*Conn) SetMaxTempDirectorySize added in v1.0.0

func (q *Conn) SetMaxTempDirectorySize(ctx context.Context, size string) error

SetMaxTempDirectorySize sets the maximum size of the temp directory (e.g. "50GB"). Defaults to 90% of available disk space.

func (*Conn) SetMemoryLimit added in v1.0.0

func (q *Conn) SetMemoryLimit(ctx context.Context, limit string) error

SetMemoryLimit sets the buffer manager's memory limit (e.g. "4GB", "512MB"). Note: actual memory consumption may exceed this limit because vectors, query results, and some aggregate states are allocated outside the buffer manager.

func (*Conn) SetPreserveInsertionOrder added in v1.0.0

func (q *Conn) SetPreserveInsertionOrder(ctx context.Context, preserve bool) error

SetPreserveInsertionOrder controls whether result ordering matches insertion order. Disabling this (false) can improve performance and reduce memory usage for larger-than-memory workloads.

func (*Conn) SetProfilingOutput added in v1.0.0

func (q *Conn) SetProfilingOutput(ctx context.Context, path string) error

SetProfilingOutput sets the file path for profiling output.

func (*Conn) SetTempDirectory added in v1.0.0

func (q *Conn) SetTempDirectory(ctx context.Context, path string) error

SetTempDirectory sets the directory used for spilling data to disk when memory is insufficient. Defaults to <database_file>.tmp for file-backed databases or .tmp for in-memory databases.

func (*Conn) SetThreads added in v1.0.0

func (q *Conn) SetThreads(ctx context.Context, n int) error

SetThreads sets the number of threads used for parallel query execution. Defaults to the number of CPU cores. Consider setting this to the number of physical cores (not hyperthreads) if hyperthreading causes slowdowns.

func (*Conn) Setting added in v1.0.0

func (q *Conn) Setting(ctx context.Context, key string) (string, error)

Setting reads the current value of a DuckDB configuration option.

Example:

threads, err := conn.Setting(ctx, "threads")
memLimit, err := conn.Setting(ctx, "memory_limit")

func (*Conn) Settings added in v1.0.0

func (q *Conn) Settings(ctx context.Context) ([]Setting, error)

Settings returns all DuckDB configuration settings with their current values, descriptions, types, and scopes.

func (*Conn) ShowAllTables added in v1.0.0

func (q *Conn) ShowAllTables(ctx context.Context) ([]TableInfo, error)

ShowAllTables returns information about all tables across all databases and schemas.

func (*Conn) ShowTables added in v1.0.0

func (q *Conn) ShowTables(ctx context.Context) ([]string, error)

ShowTables returns the names of all tables in the current schema.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE alpha (x INT)")
	conn.Exec(ctx, "CREATE TABLE beta (y INT)")

	tables, err := conn.ShowTables(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	for _, t := range tables {
		fmt.Println(t)
	}
}
Output:
alpha
beta

func (*Conn) StorageInfo added in v1.0.0

func (q *Conn) StorageInfo(ctx context.Context, table string) (*QueryResult, error)

StorageInfo returns detailed storage information for a table, including row group and compression details.

func (*Conn) Summarize added in v1.0.0

func (q *Conn) Summarize(ctx context.Context, tableOrQuery string) (*QueryResult, error)

Summarize computes aggregate statistics (min, max, approx_unique, avg, std, q25, q50, q75, count, null_percentage) over all columns of a table or query result. Returns a QueryResult for streaming access to the statistics.

func (*Conn) TableSchema added in v1.0.0

func (q *Conn) TableSchema(ctx context.Context, tableName string) (*arrow.Schema, error)

TableSchema returns the Arrow schema of a DuckDB table using the connection's default catalog and schema.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE typed (id BIGINT, name VARCHAR, active BOOLEAN)")

	schema, err := conn.TableSchema(ctx, "typed")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	for _, f := range schema.Fields() {
		fmt.Printf("%s: %s\n", f.Name, f.Type)
	}
}
Output:
id: int64
name: utf8
active: bool

func (*Conn) TableSchemaIn added in v1.0.0

func (q *Conn) TableSchemaIn(ctx context.Context, catalog, dbSchema, tableName string) (*arrow.Schema, error)

TableSchemaIn returns the Arrow schema of a DuckDB table in the specified catalog and schema. Pass empty strings for catalog and dbSchema to use the defaults.

func (*Conn) TableTypes added in v1.0.0

func (q *Conn) TableTypes(ctx context.Context) ([]string, error)

TableTypes returns the table types available in the database (e.g. "BASE TABLE", "LOCAL TEMPORARY", "VIEW").

func (*Conn) UserAgent added in v1.0.0

func (q *Conn) UserAgent(ctx context.Context) (string, error)

UserAgent returns the DuckDB user agent string (e.g. "duckdb/v1.5.1(windows_amd64)").

func (*Conn) Version added in v1.0.0

func (q *Conn) Version(ctx context.Context) (string, error)

Version returns the DuckDB version string (e.g. "v1.5.1").

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	v, err := conn.Version(context.Background())
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	// Version string starts with "v"
	fmt.Println("has version:", v != "")
}
Output:
has version: true

type Connection added in v0.5.2

type Connection = Conn

Connection is a backward-compatible alias for Conn.

type ConstraintSchema added in v0.5.3

type ConstraintSchema struct {
	ConstraintName        string        `json:"constraint_name"`
	ConstraintType        string        `json:"constraint_type"`
	ConstraintColumnNames []string      `json:"constraint_column_names"`
	ConstraintColumnUsage []UsageSchema `json:"constraint_column_usage"`
}

ConstraintSchema describes a constraint on a table (CHECK, FOREIGN KEY, PRIMARY KEY, or UNIQUE).

type DB added in v1.0.0

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

DB represents an open DuckDB database accessed through ADBC.

A DB holds the ADBC driver, database handle, and tracks all open connections. It is safe for concurrent use; internal state is protected by a sync.RWMutex. Normal operations (queries, ingests) acquire a read lock, while maintenance operations ([Compact], [ForceCheckpoint]) acquire a write lock to safely pause concurrent work.

Call NewDuck to create a DB and DB.Close to release resources.

func NewDB deprecated added in v0.5.2

func NewDB(opts ...Option) (*DB, error)

NewDB is a deprecated alias for NewDuck.

Deprecated: Use NewDuck instead.

func NewDuck

func NewDuck(opts ...Option) (*DB, error)

NewDuck opens a DuckDB database via ADBC.

Options control the database file path, driver location, and default context. If no driver option is provided, the driver name "duckdb" is passed to the ADBC driver manager, which resolves it via TOML manifests installed by the dbc CLI.

For file-backed databases, only one DB may be open per file path within a process. Attempting to open the same file twice returns ErrPathAlreadyOpen.

Example:

// In-memory database with default driver resolution:
db, err := couac.NewDuck()

// File-backed database with explicit driver path:
db, err := couac.NewDuck(
    couac.WithPath("analytics.db"),
    couac.WithDriverPath("/usr/local/lib/libduckdb.so"),
)
Example
package main

import (
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	// Open an in-memory DuckDB database with default driver resolution.
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	fmt.Println("database opened")
}
Output:
database opened
Example (FileBacked)
package main

import (
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	// Open a file-backed DuckDB database.
	db, err := couac.NewDuck(couac.WithPath("example.db"))
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	fmt.Println("file-backed database opened")
}
Output:
file-backed database opened

func NewDuckDatabase added in v1.0.0

func NewDuckDatabase(opts ...Option) (*DB, error)

NewDuckDatabase is an alias for NewDuck.

func (*DB) Checkpoint added in v1.0.0

func (q *DB) Checkpoint(ctx context.Context) error

Checkpoint synchronizes the WAL to the database file. This fails if any connections have running transactions. For a version that waits for running transactions, use DB.ForceCheckpoint.

Checkpoint acquires a read lock, allowing it to run concurrently with queries but not with maintenance operations.

func (*DB) Close added in v1.0.0

func (q *DB) Close() error

Close closes the database and all its open connections, releasing all associated resources. It is important to call Close to allow DuckDB to properly commit all WAL file changes.

Close acquires the write lock, so it waits for any in-flight operations to complete. Close is idempotent; calling it more than once returns nil.

Close returns an aggregated error if any connection or the database itself fails to close.

func (*DB) Compact added in v1.0.0

func (q *DB) Compact(ctx context.Context) error

Compact reclaims disk space by checkpointing and, for file-backed databases, creating a compacted copy.

Compact acquires the write lock on the database, which blocks until all in-flight read operations complete and prevents new operations from starting. Open connections remain valid after compaction — they are NOT closed or invalidated.

For file-backed databases, Compact:

  1. Runs FORCE CHECKPOINT to flush the WAL.
  2. ATTACHes a temporary database file.
  3. Runs COPY FROM DATABASE to create a compacted copy.
  4. DETACHes the temporary database.
  5. Replaces the original file with the compacted copy.
  6. Runs FORCE CHECKPOINT again to sync.

For in-memory databases, only FORCE CHECKPOINT is run (which reclaims space from deleted rows when the database was created with COMPRESS mode).

DuckDB's in-memory cache is preserved because at least one internal connection remains open during the operation.

func (*DB) Connect added in v1.0.0

func (q *DB) Connect() (*Conn, error)

Connect opens a new connection to the database and tracks it in the parent DB. The connection will be closed automatically when the parent's DB.Close is called, but it is good practice to close connections when no longer needed.

Connect acquires a read lock on the parent; it will block if a maintenance operation (e.g. DB.Compact) is in progress.

Example
package main

import (
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer conn.Close()

	fmt.Println("connections:", db.ConnectionCount())
}
Output:
connections: 1

func (*DB) ConnectAs added in v1.0.0

func (q *DB) ConnectAs(catalog, schema string) (*Conn, error)

ConnectAs opens a new connection with the specified catalog and schema. The catalog and schema are used as defaults for metadata and ingest operations on this connection.

Pass empty strings to use the default catalog ("memory" for in-memory databases, or the filename for file-backed databases) and default schema ("main").

func (*DB) ConnectionCount added in v1.0.0

func (q *DB) ConnectionCount() int

ConnectionCount returns the number of currently tracked open connections.

func (*DB) DefaultContext added in v1.0.0

func (q *DB) DefaultContext() context.Context

DefaultContext returns the context used for opening new connections.

func (*DB) DriverPath added in v1.0.0

func (q *DB) DriverPath() string

DriverPath returns the resolved driver path being used.

func (*DB) ForceCheckpoint added in v1.0.0

func (q *DB) ForceCheckpoint(ctx context.Context) error

ForceCheckpoint synchronizes the WAL to the database file, waiting for any running transactions to complete first (DuckDB v1.4+).

ForceCheckpoint acquires the write lock, blocking all concurrent operations until the checkpoint completes. Open connections remain valid afterward.

func (*DB) NewConnection deprecated added in v1.0.0

func (q *DB) NewConnection() (*Conn, error)

NewConnection is a backward-compatible alias for DB.Connect.

Deprecated: Use DB.Connect instead.

func (*DB) NewConnectionWithOpts deprecated added in v1.0.0

func (q *DB) NewConnectionWithOpts(catalog, schema string) (*Conn, error)

NewConnectionWithOpts is a backward-compatible alias for DB.ConnectAs.

Deprecated: Use DB.ConnectAs instead.

func (*DB) Path added in v1.0.0

func (q *DB) Path() string

Path returns the path to the database file. An empty string indicates an in-memory database.

func (*DB) Ping added in v1.0.0

func (q *DB) Ping(ctx context.Context) error

Ping verifies the database is reachable by executing SELECT 1 on a temporary connection. Returns an error if the database is closed or unreachable.

func (*DB) StdDB added in v1.0.0

func (q *DB) StdDB() *sql.DB

StdDB returns a *sql.DB backed by this DuckDB database. The returned sql.DB shares the same underlying ADBC database and connections.

This is useful for integrating with code that expects a standard *sql.DB, such as ORMs, migration tools, or test harnesses.

The returned *sql.DB should be closed when no longer needed, but closing it does NOT close the underlying couac DB.

Column values are returned as native Go types matching the Arrow column type (int8–uint64, float32/float64, bool, string, []byte, time.Time, Decimal). Nested Arrow types are returned as List, Struct, and Map with full recursive conversion to Go-native values. See [arrowToDriverValue] for the complete mapping.

Parameterized queries are supported with both ? and $N placeholder styles (e.g. "SELECT * FROM t WHERE id = ?" or "WHERE id = $1"). Named parameters ($name) are not supported by DuckDB's ADBC driver. Supported Go parameter types: int64, float64, bool, string, []byte, time.Time, Decimal, and nil.

Limitations of the database/sql bridge:

Example:

db, _ := couac.NewDuck()
defer db.Close()

stdDB := db.StdDB()
defer stdDB.Close()

var count int
stdDB.QueryRowContext(ctx, "SELECT count(*) FROM users").Scan(&count)
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	// Set up data via the native API.
	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE stddb_demo (id INT, name VARCHAR)")
	conn.Exec(ctx, "INSERT INTO stddb_demo VALUES (1, 'Pierre'), (2, 'Jean'), (3, 'Jacques')")
	conn.Close()

	// Use database/sql for querying.
	stdDB := db.StdDB()
	defer stdDB.Close()

	var count int
	if err := stdDB.QueryRowContext(ctx, "SELECT count(*) FROM stddb_demo").Scan(&count); err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("count:", count)
}
Output:
count: 3
Example (NativeTypes)
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE products (name VARCHAR, price DECIMAL(10,2), in_stock BOOLEAN)")
	conn.Exec(ctx, "INSERT INTO products VALUES ('Widget', 19.99, true), ('Gadget', 49.50, false)")
	conn.Close()

	// Values come back as native Go types, not strings.
	stdDB := db.StdDB()
	defer stdDB.Close()

	rows, err := stdDB.QueryContext(ctx, "SELECT name, price, in_stock FROM products ORDER BY name")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var name string
		var price couac.Decimal
		var inStock bool
		if err := rows.Scan(&name, &price, &inStock); err != nil {
			fmt.Println("Error:", err)
			return
		}
		fmt.Printf("%s: $%s (in stock: %v)\n", name, price.String(), inStock)
	}
}
Output:
Gadget: $49.50 (in stock: false)
Widget: $19.99 (in stock: true)
Example (ParameterizedQuery)
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE inventory (id INT, item VARCHAR, qty INT)")
	conn.Exec(ctx, "INSERT INTO inventory VALUES (1,'bolt',100),(2,'nut',250),(3,'screw',75)")
	conn.Close()

	// Use parameterized queries with ? placeholders.
	stdDB := db.StdDB()
	defer stdDB.Close()

	var item string
	err = stdDB.QueryRowContext(ctx,
		"SELECT item FROM inventory WHERE qty > ? ORDER BY qty LIMIT 1",
		int64(80),
	).Scan(&item)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("item:", item)

	// $N placeholders also work.
	var qty int
	err = stdDB.QueryRowContext(ctx,
		"SELECT qty FROM inventory WHERE item = $1",
		"nut",
	).Scan(&qty)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("qty:", qty)
}
Output:
item: bolt
qty: 250
Example (PreparedStatement)
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()

	ctx := context.Background()
	stdDB.ExecContext(ctx, "CREATE TABLE scores (player VARCHAR, score INT)")

	// Prepare a statement once, execute it multiple times with different parameters.
	stmt, err := stdDB.PrepareContext(ctx, "INSERT INTO scores VALUES (?, ?)")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer stmt.Close()

	for _, p := range []struct {
		name  string
		score int64
	}{
		{"Alice", 95},
		{"Bob", 87},
		{"Carol", 92},
	} {
		if _, err := stmt.ExecContext(ctx, p.name, p.score); err != nil {
			fmt.Println("Error:", err)
			return
		}
	}

	var count int
	stdDB.QueryRowContext(ctx, "SELECT count(*) FROM scores").Scan(&count)
	fmt.Println("players:", count)

	var topName string
	stdDB.QueryRowContext(ctx, "SELECT player FROM scores ORDER BY score DESC LIMIT 1").Scan(&topName)
	fmt.Println("top scorer:", topName)
}
Output:
players: 3
top scorer: Alice
Example (Transaction)
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()

	ctx := context.Background()
	stdDB.ExecContext(ctx, "CREATE TABLE ledger (acct VARCHAR, balance INT)")
	stdDB.ExecContext(ctx, "INSERT INTO ledger VALUES ('checking', 1000), ('savings', 500)")

	// Execute a transfer inside a transaction.
	tx, err := stdDB.BeginTx(ctx, nil)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	tx.ExecContext(ctx, "UPDATE ledger SET balance = balance - ? WHERE acct = ?", int64(200), "checking")
	tx.ExecContext(ctx, "UPDATE ledger SET balance = balance + ? WHERE acct = ?", int64(200), "savings")
	if err := tx.Commit(); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Verify balances.
	rows, _ := stdDB.QueryContext(ctx, "SELECT acct, balance FROM ledger ORDER BY acct")
	defer rows.Close()
	for rows.Next() {
		var acct string
		var bal int
		rows.Scan(&acct, &bal)
		fmt.Printf("%s: %d\n", acct, bal)
	}
}
Output:
checking: 800
savings: 700

func (*DB) WithTransaction added in v1.0.0

func (q *DB) WithTransaction(ctx context.Context, fn func(*Conn) error) (retErr error)

WithTransaction executes fn within a transaction. It opens a dedicated connection, disables auto-commit, executes fn, and then either commits (if fn returns nil) or rolls back (if fn returns an error or panics). The dedicated connection is closed after the transaction completes.

DuckDB uses optimistic concurrency control. If fn modifies rows that are concurrently modified by another transaction, DuckDB may return a "Transaction conflict" error.

Example:

err := db.WithTransaction(ctx, func(tx *couac.Conn) error {
    _, err := tx.Exec(ctx, "INSERT INTO t1 VALUES (1)")
    if err != nil { return err }
    _, err = tx.Exec(ctx, "INSERT INTO t2 VALUES (2)")
    return err
})
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	ctx := context.Background()

	// Set up tables via a regular connection.
	setup, _ := db.Connect()
	setup.Exec(ctx, "CREATE TABLE account (id INT, balance INT)")
	setup.Exec(ctx, "INSERT INTO account VALUES (1, 1000), (2, 500)")
	setup.Close()

	// Run a transfer inside a transaction.
	err = db.WithTransaction(ctx, func(tx *couac.Conn) error {
		_, err := tx.Exec(ctx, "UPDATE account SET balance = balance - 200 WHERE id = 1")
		if err != nil {
			return err
		}
		_, err = tx.Exec(ctx, "UPDATE account SET balance = balance + 200 WHERE id = 2")
		return err
	})
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Verify the transfer.
	verify, _ := db.Connect()
	defer verify.Close()
	res, _ := verify.Query(ctx, "SELECT id, balance FROM account ORDER BY id")
	defer res.Close()
	for res.Reader.Next() {
		rec := res.Reader.RecordBatch()
		for i := 0; i < int(rec.NumRows()); i++ {
			fmt.Printf("account %s: balance %s\n",
				rec.Column(0).ValueStr(i), rec.Column(1).ValueStr(i))
		}
	}
}
Output:
account 1: balance 800
account 2: balance 700

type DBObject added in v0.5.3

type DBObject = CatalogInfo

DBObject is a backward-compatible alias for CatalogInfo.

type DBObjects added in v1.0.0

type DBObjects = CatalogTree

DBObjects is a backward-compatible alias for CatalogTree.

type DBSchema added in v0.5.3

type DBSchema = SchemaInfo

DBSchema is a backward-compatible alias for SchemaInfo.

type DatabaseSize added in v1.0.0

type DatabaseSize struct {
	DatabaseName string `json:"database_name"`
	DatabaseSize string `json:"database_size"`
	BlockSize    int64  `json:"block_size"`
	TotalBlocks  int64  `json:"total_blocks"`
	UsedBlocks   int64  `json:"used_blocks"`
	FreeBlocks   int64  `json:"free_blocks"`
	WALSize      string `json:"wal_size"`
	MemoryUsage  string `json:"memory_usage"`
	MemoryLimit  string `json:"memory_limit"`
}

DatabaseSize describes the on-disk size information for a database.

type Decimal added in v1.0.0

type Decimal struct {
	Width    uint8
	Scale    uint8
	Unscaled *big.Int
}

Decimal represents an arbitrary-precision decimal number. Width is the total number of digits, Scale is the number of digits after the decimal point, and Unscaled is the integer value before applying the scale (i.e. the actual value is Unscaled / 10^Scale).

This mirrors the Decimal type used by duckdb-go for compatibility.

Usage with database/sql:

var d couac.Decimal
row.Scan(&d)
precise := d.BigFloat()    // lossless *big.Float
approx  := d.Float64()     // lossy float64
text    := d.String()      // "123.45"
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE amounts (v DECIMAL(18,4))")
	conn.Exec(ctx, "INSERT INTO amounts VALUES (123456.7890)")
	conn.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()

	var d couac.Decimal
	if err := stdDB.QueryRowContext(ctx, "SELECT v FROM amounts").Scan(&d); err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("String:", d.String())
	fmt.Println("Float64:", d.Float64())
	fmt.Println("BigFloat:", d.BigFloat().Text('f', 4))
}
Output:
String: 123456.7890
Float64: 123456.789
BigFloat: 123456.7890

func (Decimal) BigFloat added in v1.0.0

func (d Decimal) BigFloat() *big.Float

BigFloat converts the Decimal to a *big.Float with full precision. This is lossless for all decimal values that DuckDB can produce.

Usage with database/sql:

var d couac.Decimal
row.Scan(&d)
precise := d.BigFloat()

func (Decimal) Float64 added in v1.0.0

func (d Decimal) Float64() float64

Float64 converts the Decimal to a float64. This may lose precision for high-precision values; use Decimal.BigFloat when precision matters.

func (*Decimal) Scan added in v1.0.0

func (d *Decimal) Scan(src any) error

Scan implements sql.Scanner for Decimal. It accepts the following source types:

  • Decimal: direct assignment
  • *big.Int: assigns Unscaled directly (Scale=0)
  • int64, float64: converts to Decimal with Scale=0
  • string, []byte: parses as a decimal string (e.g. "123.45")
  • nil: zeroes the Decimal

func (Decimal) String added in v1.0.0

func (d Decimal) String() string

String formats the Decimal as a fixed-point string with the correct number of decimal places (e.g. "123.45" for Value=12345, Scale=2).

func (Decimal) Value added in v1.0.0

func (d Decimal) Value() (driver.Value, error)

Value implements database/sql/driver.Valuer, allowing Decimal to be passed as a query parameter. It serializes as the fixed-point string representation (e.g. "123.45").

type DuckDatabase added in v0.5.2

type DuckDatabase = DB

DuckDatabase is a backward-compatible alias for DB.

type Extension added in v1.0.0

type Extension struct {
	Name        string `json:"extension_name"`
	Loaded      bool   `json:"loaded"`
	Installed   bool   `json:"installed"`
	Version     string `json:"extension_version"`
	Description string `json:"description"`
	InstallMode string `json:"install_mode"`
}

Extension describes an installed DuckDB extension.

type GetObjectsOption added in v1.0.0

type GetObjectsOption = ObjectsOption

GetObjectsOption is a backward-compatible alias for ObjectsOption.

type List added in v1.0.0

type List struct {
	Values []any
}

List represents a DuckDB LIST column value as a Go slice. Elements are Go-native types produced by the recursive Arrow-to-Go conversion: int8–uint64, float32, float64, bool, string, []byte, Decimal, nested List, Struct, Map, or nil.

List also represents LargeList and FixedSizeList columns.

List implements sql.Scanner and driver.Valuer.

Usage with database/sql:

var l couac.List
row.Scan(&l)
fmt.Println(l.Values)       // []any{1, 2, 3}
ints := l.Ints()            // []int64{1, 2, 3}
strs := l.Strings()         // []string{"1", "2", "3"}
j, _ := l.MarshalJSON()     // [1,2,3]

Typed element access:

n, ok := l.Int32At(0)                  // leaf value
s, ok := l.StringAt(2)                 // leaf value
n, ok  = couac.Elem[int32](l, 0)       // generic (any leaf type)

Chaining into nested types — navigation methods return a zero value on failure, so further calls safely return zero/false:

name, ok := outer.StructAt(0).Str("name")    // LIST<STRUCT<…>>
ints     := outer.ListAt(0).Ints()             // LIST<LIST<INT>>
v, ok    := outer.MapAt(0).Int32("key")        // LIST<MAP<…>>
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	// Scan a DuckDB list column into a couac.List.
	var l couac.List
	err = stdDB.QueryRowContext(ctx, "SELECT [10, 20, 30]::INTEGER[]").Scan(&l)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("values:", l.Values)
	fmt.Println("ints:", l.Ints())
	fmt.Println("floats:", l.Floats())
	fmt.Println("strings:", l.Strings())
	fmt.Println("json:", l.String())
}
Output:
values: [10 20 30]
ints: [10 20 30]
floats: [10 20 30]
strings: [10 20 30]
json: [10,20,30]
Example (Nested)

ExampleList_nested demonstrates scanning a LIST of LIST (nested list) and using Scan to navigate into inner lists with convenience accessors.

package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	var outer couac.List
	err = stdDB.QueryRowContext(ctx, "SELECT [[1, 2], [3, 4, 5]]::INTEGER[][]").Scan(&outer)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("outer length:", len(outer.Values))

	// Chain ListAt directly into Ints — no intermediate variables
	fmt.Println("inner[0] ints:", outer.ListAt(0).Ints())
	fmt.Println("inner[1] ints:", outer.ListAt(1).Ints())
}
Output:
outer length: 2
inner[0] ints: [1 2]
inner[1] ints: [3 4 5]
Example (NestedStructWithList)

ExampleList_nestedStructWithList demonstrates depth-3 chaining: StructAt → Str / List → Ints in a single expression.

package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	var outer couac.List
	err = stdDB.QueryRowContext(ctx,
		"SELECT [{'name': 'a', 'vals': [1, 2]}, {'name': 'b', 'vals': [3]}]::"+
			"STRUCT(name VARCHAR, vals INTEGER[])[]",
	).Scan(&outer)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	for i := range outer.Values {
		name, _ := outer.StructAt(i).Str("name")
		vals := outer.StructAt(i).List("vals").Ints()
		fmt.Printf("elem[%d]: name=%s vals=%v\n", i, name, vals)
	}
}
Output:
elem[0]: name=a vals=[1 2]
elem[1]: name=b vals=[3]
Example (NullElements)

ExampleList_nullElements demonstrates that NULL elements inside a LIST are detected by the ok return from Int32At.

package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	var l couac.List
	err = stdDB.QueryRowContext(ctx, "SELECT [1, NULL, 3]::INTEGER[]").Scan(&l)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	for i := range l.Values {
		v, ok := l.Int32At(i)
		if ok {
			fmt.Printf("[%d] = %d\n", i, v)
		} else {
			fmt.Printf("[%d] = NULL\n", i)
		}
	}
}
Output:
[0] = 1
[1] = NULL
[2] = 3

func (List) BoolAt added in v1.1.0

func (l List) BoolAt(i int) (bool, bool)

BoolAt returns the element at index i as bool. Returns (false, false) if i is out of range, nil, or not bool.

func (List) Bools added in v1.0.0

func (l List) Bools() []bool

Bools returns the list elements as []bool. Elements that are not bool are skipped.

func (List) Float64At added in v1.1.0

func (l List) Float64At(i int) (float64, bool)

Float64At returns the element at index i as float64. Returns (0, false) if i is out of range, nil, or not float64.

func (List) Floats added in v1.0.0

func (l List) Floats() []float64

Floats returns the list elements as []float64. Elements that are not numeric types are skipped.

func (List) Int32At added in v1.1.0

func (l List) Int32At(i int) (int32, bool)

Int32At returns the element at index i as int32. Returns (0, false) if i is out of range, nil, or not int32.

func (List) Int64At added in v1.1.0

func (l List) Int64At(i int) (int64, bool)

Int64At returns the element at index i as int64. Returns (0, false) if i is out of range, nil, or not int64.

func (List) Ints added in v1.0.0

func (l List) Ints() []int64

Ints returns the list elements as []int64. Elements that are not integer or float types are skipped. This is a convenience accessor for lists known to contain only integers (TINYINT through UBIGINT).

func (List) JSON added in v1.0.0

func (l List) JSON() List

JSON returns a copy of the List with every element converted to its JSON string representation. This is useful when you want to process nested structures as JSON text rather than Go maps/slices.

func (List) ListAt added in v1.1.0

func (l List) ListAt(i int) List

ListAt returns the element at index i as a List, enabling chaining. Returns a zero List if i is out of range, nil, or not a list; further calls on the zero value safely return zero/false.

func (List) MapAt added in v1.1.0

func (l List) MapAt(i int) Map

MapAt returns the element at index i as a Map, enabling chaining. Returns a zero Map if i is out of range, nil, or not a map; further calls on the zero value safely return zero/false.

func (List) MarshalJSON added in v1.0.0

func (l List) MarshalJSON() ([]byte, error)

MarshalJSON implements [json.Marshaler].

func (*List) Scan added in v1.0.0

func (l *List) Scan(src any) error

Scan implements sql.Scanner for List. It accepts:

  • List: direct assignment
  • []any: wraps directly
  • string, []byte: parses as JSON array
  • nil: zeroes the list

func (List) String added in v1.0.0

func (l List) String() string

String returns the JSON representation of the list.

func (List) StringAt added in v1.1.0

func (l List) StringAt(i int) (string, bool)

StringAt returns the element at index i as string. Returns ("", false) if i is out of range, nil, or not a string.

func (List) Strings added in v1.0.0

func (l List) Strings() []string

Strings returns the list elements as []string. Each element is formatted with fmt.Sprint. Nil elements become the empty string.

func (List) StructAt added in v1.1.0

func (l List) StructAt(i int) Struct

StructAt returns the element at index i as a Struct, enabling chaining. Returns a zero Struct if i is out of range, nil, or not a struct; further calls on the zero value safely return zero/false.

func (List) Value added in v1.0.0

func (l List) Value() (driver.Value, error)

Value implements driver.Valuer. It serializes the list as a JSON string.

type Map added in v1.0.0

type Map struct {
	Values map[string]any
}

Map represents a DuckDB MAP column value as a Go map. DuckDB MAP keys are typically VARCHAR; non-string keys are converted with fmt.Sprint. Values are Go-native types produced by the recursive Arrow-to-Go conversion.

Map implements sql.Scanner and driver.Valuer.

Usage with database/sql:

var m couac.Map
row.Scan(&m)
fmt.Println(m.Values["key1"]) // 42
j, _ := m.MarshalJSON()       // {"key1":42,"key2":99}

Typed value access:

count, ok := m.Int32("key1")                // int32 value
name, ok  := m.Str("key2")                  // string value
count, ok  = couac.Value[int32](m, "key1")  // generic (any leaf type)

Chaining into nested types — navigation methods return a zero value on failure, so further calls safely return zero/false:

age, ok := m.Struct("alice").Int32("age")   // STRUCT value → Struct
nums    := m.List("nums").Ints()              // LIST value → List
v, ok   := m.Map("sub").Str("key")            // MAP value → Map
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	// Scan a DuckDB map column into a couac.Map.
	var m couac.Map
	err = stdDB.QueryRowContext(ctx, "SELECT MAP {'a': 1, 'b': 2}").Scan(&m)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	a, _ := m.Get("a")
	b, _ := m.Get("b")
	fmt.Println("a:", a)
	fmt.Println("b:", b)
}
Output:
a: 1
b: 2
Example (NestedStructValues)

ExampleMap_nestedStructValues demonstrates chaining into MAP values that are STRUCTs: Struct("key") → Int32("field").

package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	var m couac.Map
	err = stdDB.QueryRowContext(ctx,
		"SELECT MAP {'alice': {'age': 30}, 'bob': {'age': 25}}::"+
			"MAP(VARCHAR, STRUCT(age INTEGER))",
	).Scan(&m)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Chain: Map → Struct → Int32
	age, _ := m.Struct("alice").Int32("age")
	fmt.Println("alice age:", age)

	bobAge, _ := m.Struct("bob").Int32("age")
	fmt.Println("bob age:", bobAge)
}
Output:
alice age: 30
bob age: 25

func (Map) Bool added in v1.1.0

func (m Map) Bool(key string) (bool, bool)

Bool returns the value for key as bool. Returns (false, false) if the key is missing, nil, or not bool.

func (Map) Float64 added in v1.1.0

func (m Map) Float64(key string) (float64, bool)

Float64 returns the value for key as float64. Returns (0, false) if the key is missing, nil, or not float64.

func (Map) Get added in v1.0.0

func (m Map) Get(key string) (any, bool)

Get returns the value for the given key and whether the key exists in the map. This is a convenience shorthand for m.Values[key].

func (Map) Int32 added in v1.1.0

func (m Map) Int32(key string) (int32, bool)

Int32 returns the value for key as int32. Returns (0, false) if the key is missing, nil, or not int32.

func (Map) Int64 added in v1.1.0

func (m Map) Int64(key string) (int64, bool)

Int64 returns the value for key as int64. Returns (0, false) if the key is missing, nil, or not int64.

func (Map) JSON added in v1.0.0

func (m Map) JSON() Map

JSON returns a copy of the Map with every value converted to its JSON string representation.

func (Map) Keys added in v1.0.0

func (m Map) Keys() []string

Keys returns the map keys as a string slice.

func (Map) List added in v1.1.0

func (m Map) List(key string) List

List returns the value for key as a List, enabling chaining. Returns a zero List if the key is missing, nil, or not a list; further calls on the zero value safely return zero/false.

func (Map) Map added in v1.1.0

func (m Map) Map(key string) Map

Map returns the value for key as a Map, enabling chaining. Returns a zero Map if the key is missing, nil, or not a map; further calls on the zero value safely return zero/false.

func (Map) MarshalJSON added in v1.0.0

func (m Map) MarshalJSON() ([]byte, error)

MarshalJSON implements [json.Marshaler].

func (*Map) Scan added in v1.0.0

func (m *Map) Scan(src any) error

Scan implements sql.Scanner for Map. It accepts:

  • Map: direct assignment
  • map[string]any: wraps directly
  • string, []byte: parses as JSON object
  • nil: zeroes the map

func (Map) Str added in v1.1.0

func (m Map) Str(key string) (string, bool)

Str returns the value for key as string. Returns ("", false) if the key is missing, nil, or not a string. (Named Str to avoid collision with the String() string method.)

func (Map) String added in v1.0.0

func (m Map) String() string

String returns the JSON representation of the map.

func (Map) Struct added in v1.1.0

func (m Map) Struct(key string) Struct

Struct returns the value for key as a Struct, enabling chaining. Returns a zero Struct if the key is missing, nil, or not a struct; further calls on the zero value safely return zero/false.

func (Map) Value added in v1.0.0

func (m Map) Value() (driver.Value, error)

Value implements driver.Valuer. It serializes the map as a JSON string.

type NullDecimal added in v1.0.0

type NullDecimal struct {
	Decimal Decimal
	Valid   bool // Valid is true if Decimal is not NULL
}

NullDecimal represents a Decimal that may be null. NullDecimal implements sql.Scanner and driver.Valuer, following the same pattern as sql.NullString, sql.NullInt64, and sql.NullFloat64.

Usage with database/sql:

var nd couac.NullDecimal
err := row.Scan(&nd)
if nd.Valid {
    fmt.Println(nd.Decimal.BigFloat())
}
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	conn, err := db.Connect()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	ctx := context.Background()
	conn.Exec(ctx, "CREATE TABLE nullable_amounts (v DECIMAL(10,2))")
	conn.Exec(ctx, "INSERT INTO nullable_amounts VALUES (42.00), (NULL)")
	conn.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()

	rows, err := stdDB.QueryContext(ctx, "SELECT v FROM nullable_amounts ORDER BY v NULLS FIRST")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var nd couac.NullDecimal
		if err := rows.Scan(&nd); err != nil {
			fmt.Println("Error:", err)
			return
		}
		if nd.Valid {
			fmt.Println("value:", nd.Decimal.String())
		} else {
			fmt.Println("value: NULL")
		}
	}
}
Output:
value: NULL
value: 42.00

func (*NullDecimal) Scan added in v1.0.0

func (nd *NullDecimal) Scan(src any) error

Scan implements sql.Scanner. It sets Valid to false for nil values and delegates to Decimal.Scan otherwise.

func (NullDecimal) Value added in v1.0.0

func (nd NullDecimal) Value() (driver.Value, error)

Value implements driver.Valuer. It returns nil when !Valid and delegates to Decimal.Value otherwise.

type NullList added in v1.0.0

type NullList struct {
	List  List
	Valid bool // Valid is true if List is not NULL
}

NullList represents a List that may be null, following the same pattern as sql.NullString.

Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	stdDB.ExecContext(ctx, "CREATE TABLE nl (tags INTEGER[])")
	stdDB.ExecContext(ctx, "INSERT INTO nl VALUES ([1,2]), (NULL)")

	rows, _ := stdDB.QueryContext(ctx, "SELECT tags FROM nl ORDER BY tags NULLS LAST")
	defer rows.Close()

	for rows.Next() {
		var nl couac.NullList
		rows.Scan(&nl)
		fmt.Println("valid:", nl.Valid)
	}
}
Output:
valid: true
valid: false

func (*NullList) Scan added in v1.0.0

func (nl *NullList) Scan(src any) error

Scan implements sql.Scanner.

func (NullList) Value added in v1.0.0

func (nl NullList) Value() (driver.Value, error)

Value implements driver.Valuer.

type NullMap added in v1.0.0

type NullMap struct {
	Map   Map
	Valid bool // Valid is true if Map is not NULL
}

NullMap represents a Map that may be null, following the same pattern as sql.NullString.

func (*NullMap) Scan added in v1.0.0

func (nm *NullMap) Scan(src any) error

Scan implements sql.Scanner.

func (NullMap) Value added in v1.0.0

func (nm NullMap) Value() (driver.Value, error)

Value implements driver.Valuer.

type NullStruct added in v1.0.0

type NullStruct struct {
	Struct Struct
	Valid  bool // Valid is true if Struct is not NULL
}

NullStruct represents a Struct that may be null, following the same pattern as sql.NullString.

func (*NullStruct) Scan added in v1.0.0

func (ns *NullStruct) Scan(src any) error

Scan implements sql.Scanner.

func (NullStruct) Value added in v1.0.0

func (ns NullStruct) Value() (driver.Value, error)

Value implements driver.Valuer.

type ObjectDepth added in v0.2.0

type ObjectDepth int

ObjectDepth controls how deep Conn.Objects recurses into the catalog hierarchy.

const (
	// ObjectDepthAll requests catalogs, schemas, tables, and columns.
	ObjectDepthAll ObjectDepth = iota
	// ObjectDepthCatalogs requests only catalog names.
	ObjectDepthCatalogs
	// ObjectDepthDBSchemas requests catalogs and their schemas.
	ObjectDepthDBSchemas
	// ObjectDepthTables requests catalogs, schemas, and tables (no columns).
	ObjectDepthTables
	// ObjectDepthColumns is an alias for ObjectDepthAll.
	ObjectDepthColumns = ObjectDepthAll
)

type ObjectsOption added in v1.0.0

type ObjectsOption func(*objectsConfig)

ObjectsOption configures a Conn.Objects call.

func WithCatalogFilter added in v1.0.0

func WithCatalogFilter(catalog string) ObjectsOption

WithCatalogFilter filters Objects results to catalogs matching the given name. An empty string matches only objects without a catalog.

func WithColumnFilter added in v1.0.0

func WithColumnFilter(column string) ObjectsOption

WithColumnFilter filters Objects results to columns matching the given name.

func WithDepth added in v1.0.0

func WithDepth(depth ObjectDepth) ObjectsOption

WithDepth sets the ObjectDepth for an Objects call, controlling how deep to recurse into the catalog hierarchy.

func WithSchemaFilter added in v1.0.0

func WithSchemaFilter(schema string) ObjectsOption

WithSchemaFilter filters Objects results to schemas matching the given name.

func WithTableFilter added in v1.0.0

func WithTableFilter(table string) ObjectsOption

WithTableFilter filters Objects results to tables matching the given name.

func WithTableTypes added in v1.0.0

func WithTableTypes(types []string) ObjectsOption

WithTableTypes filters Objects results to the given table types (e.g. []string{"TABLE", "VIEW"}).

type Option added in v0.4.0

type Option func(config)

Option configures a DB during construction via NewDuck.

func WithContext added in v0.5.0

func WithContext(ctx context.Context) Option

WithContext sets the default context used for opening new connections. If omitted, context.Background is used.

func WithDriverLookup added in v1.0.0

func WithDriverLookup() Option

WithDriverLookup programmatically searches ADBC driver manifest directories for an installed "duckdb" driver, using the github.com/columnar-tech/dbc/config package. It searches in order:

  1. ADBC_DRIVER_PATH environment variable
  2. Virtual environment / Conda prefix
  3. User config directory (~/.config/adbc/drivers on Linux, ~/Library/Application Support/ADBC/Drivers on macOS, %LOCALAPPDATA%\ADBC\Drivers on Windows)
  4. System config directory (/etc/adbc/drivers on Linux, C:\Program Files\ADBC\Drivers on Windows)

If the driver is not found, NewDuck returns ErrDriverNotFound. The driver must first be installed with:

dbc install duckdb

func WithDriverName added in v1.0.0

func WithDriverName(name string) Option

WithDriverName specifies the driver by name (e.g. "duckdb"). The ADBC driver manager resolves the name to a shared library using installed TOML driver manifests. This requires that the driver has been installed with the dbc CLI:

dbc install duckdb

This is the default if no driver option is provided.

func WithDriverPath added in v0.4.0

func WithDriverPath(path string) Option

WithDriverPath specifies the explicit filesystem path to the DuckDB shared library (e.g. "/usr/local/lib/libduckdb.so" or "C:\\path\\to\\duckdb.dll"). Use this as an escape hatch when the driver is not installed via dbc.

func WithPath added in v0.4.0

func WithPath(path string) Option

WithPath sets the database file path. If omitted or empty, the database is created in-memory.

type QuackCon added in v0.3.0

type QuackCon = Conn

QuackCon is a backward-compatible alias for Conn.

type Quacker

type Quacker = DB

Quacker is a backward-compatible alias for DB.

type QueryResult added in v1.0.0

type QueryResult struct {
	// Reader provides streaming access to Arrow record batches.
	Reader array.RecordReader

	// RowsAffected is the number of rows affected, or -1 if unknown.
	RowsAffected int64
	// contains filtered or unexported fields
}

QueryResult holds the results of a Conn.Query call.

The Reader field provides streaming access to Arrow record batches. RowsAffected contains the number of rows affected if known, otherwise -1. The caller must call Close when done reading to release resources.

Since ADBC 1.1.0, releasing the Reader without fully consuming it is equivalent to calling AdbcStatementCancel.

func (*QueryResult) Close added in v1.0.0

func (qr *QueryResult) Close() error

Close releases the resources associated with the query result. It closes both the underlying statement and the record reader. Close is idempotent.

func (*QueryResult) Schema added in v1.0.0

func (qr *QueryResult) Schema() *arrow.Schema

Schema returns the Arrow schema of the result set, or nil if the Reader has been closed.

type SchemaInfo added in v1.0.0

type SchemaInfo struct {
	DBSchemaName   string        `json:"db_schema_name"`
	DBSchemaTables []TableSchema `json:"db_schema_tables"`
}

SchemaInfo represents a schema within a catalog.

type Secret added in v1.0.0

type Secret struct {
	Name     string `json:"name"`
	Type     string `json:"type"`
	Provider string `json:"provider"`
	Scope    string `json:"scope"`
}

Secret describes a stored DuckDB secret. Sensitive fields are redacted by DuckDB.

type Setting added in v1.0.0

type Setting struct {
	Name        string `json:"name"`
	Value       string `json:"value"`
	Description string `json:"description"`
	InputType   string `json:"input_type"`
	Scope       string `json:"scope"`
}

Setting describes a DuckDB configuration setting.

type Statement added in v0.2.0

type Statement = adbc.Statement

Statement is an alias for adbc.Statement.

type Struct added in v1.0.0

type Struct struct {
	Fields map[string]any
}

Struct represents a DuckDB STRUCT column value as a Go map. Field names are the map keys; values are Go-native types produced by the recursive Arrow-to-Go conversion.

Struct implements sql.Scanner and driver.Valuer.

Usage with database/sql:

var s couac.Struct
row.Scan(&s)
fmt.Println(s.Fields["name"]) // "Alice"
j, _ := s.MarshalJSON()       // {"name":"Alice","age":30}

Typed field access:

name, ok := s.Str("name")                   // string field
age, ok  := s.Int32("age")                   // int32 field
name, ok  = couac.Field[string](s, "name")   // generic (any leaf type)

Chaining into nested types — navigation methods return a zero value on failure, so further calls safely return zero/false:

city, ok := s.Struct("address").Str("city")  // STRUCT field → Struct
vals     := s.List("tags").Ints()              // LIST field → List
v, ok    := s.Map("attrs").Int32("k1")         // MAP field → Map
Example
package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	// Scan a DuckDB struct column into a couac.Struct.
	var s couac.Struct
	err = stdDB.QueryRowContext(ctx,
		"SELECT {'name': 'Alice', 'age': 30}::STRUCT(name VARCHAR, age INTEGER)",
	).Scan(&s)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	name, _ := s.Get("name")
	age, _ := s.Get("age")
	fmt.Println("name:", name)
	fmt.Println("age:", age)
}
Output:
name: Alice
age: 30
Example (Nested)

ExampleStruct_nested demonstrates chaining into a nested STRUCT: Struct("field") returns a Struct you can immediately call Str/Int32/etc on.

package main

import (
	"context"
	"fmt"

	"github.com/loicalleyne/couac"
)

func main() {
	db, err := couac.NewDuck()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer db.Close()

	stdDB := db.StdDB()
	defer stdDB.Close()
	ctx := context.Background()

	var s couac.Struct
	err = stdDB.QueryRowContext(ctx,
		"SELECT {'name': 'Alice', 'address': {'city': 'Montreal', 'zip': '12345'}}::"+
			"STRUCT(name VARCHAR, address STRUCT(city VARCHAR, zip VARCHAR))",
	).Scan(&s)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	name, _ := s.Str("name")
	fmt.Println("name:", name)

	// Chain: Struct → Struct → Str
	city, _ := s.Struct("address").Str("city")
	zip, _ := s.Struct("address").Str("zip")
	fmt.Println("city:", city)
	fmt.Println("zip:", zip)
}
Output:
name: Alice
city: Montreal
zip: 12345

func (Struct) Bool added in v1.1.0

func (s Struct) Bool(name string) (bool, bool)

Bool returns the named field as bool. Returns (false, false) if the field is missing, nil, or not bool.

func (Struct) Float64 added in v1.1.0

func (s Struct) Float64(name string) (float64, bool)

Float64 returns the named field as float64. Returns (0, false) if the field is missing, nil, or not float64.

func (Struct) Get added in v1.0.0

func (s Struct) Get(name string) (any, bool)

Get returns the value for the given field name and whether the field exists in the struct. This is a convenience shorthand for s.Fields[name].

func (Struct) Int32 added in v1.1.0

func (s Struct) Int32(name string) (int32, bool)

Int32 returns the named field as int32. Returns (0, false) if the field is missing, nil, or not int32.

func (Struct) Int64 added in v1.1.0

func (s Struct) Int64(name string) (int64, bool)

Int64 returns the named field as int64. Returns (0, false) if the field is missing, nil, or not int64.

func (Struct) JSON added in v1.0.0

func (s Struct) JSON() Struct

JSON returns a copy of the Struct with every field value converted to its JSON string representation. This is useful when you want to process nested structures as JSON text.

func (Struct) List added in v1.1.0

func (s Struct) List(name string) List

List returns the named field as a List, enabling chaining. Returns a zero List if the field is missing, nil, or not a list; further calls on the zero value safely return zero/false.

func (Struct) Map added in v1.1.0

func (s Struct) Map(name string) Map

Map returns the named field as a Map, enabling chaining. Returns a zero Map if the field is missing, nil, or not a map; further calls on the zero value safely return zero/false.

func (Struct) MarshalJSON added in v1.0.0

func (s Struct) MarshalJSON() ([]byte, error)

MarshalJSON implements [json.Marshaler].

func (*Struct) Scan added in v1.0.0

func (s *Struct) Scan(src any) error

Scan implements sql.Scanner for Struct. It accepts:

  • Struct: direct assignment
  • map[string]any: wraps directly
  • string, []byte: parses as JSON object
  • nil: zeroes the struct

func (Struct) Str added in v1.1.0

func (s Struct) Str(name string) (string, bool)

Str returns the named field as string. Returns ("", false) if the field is missing, nil, or not a string. (Named Str to avoid collision with the String() string method.)

func (Struct) String added in v1.0.0

func (s Struct) String() string

String returns the JSON representation of the struct.

func (Struct) Struct added in v1.1.0

func (s Struct) Struct(name string) Struct

Struct returns the named field as a Struct, enabling chaining. Returns a zero Struct if the field is missing, nil, or not a struct; further calls on the zero value safely return zero/false.

func (Struct) Value added in v1.0.0

func (s Struct) Value() (driver.Value, error)

Value implements driver.Valuer. It serializes the struct as a JSON string.

type TableInfo added in v1.0.0

type TableInfo struct {
	Database    string   `json:"database"`
	Schema      string   `json:"schema"`
	TableName   string   `json:"table_name"`
	ColumnNames []string `json:"column_names"`
	ColumnTypes []string `json:"column_types"`
	Temporary   bool     `json:"temporary"`
}

TableInfo describes a table as returned by SHOW ALL TABLES.

type TableSchema added in v0.5.3

type TableSchema struct {
	TableName        string             `json:"table_name"`
	TableType        string             `json:"table_type"`
	TableColumns     []ColumnSchema     `json:"table_columns"`
	TableConstraints []ConstraintSchema `json:"table_constraints"`
}

TableSchema describes a table or view within a schema.

type UsageSchema added in v0.5.3

type UsageSchema struct {
	FKCatalog    string `json:"fk_catalog"`
	FKDBSchema   string `json:"fk_db_schema"`
	FKTable      string `json:"fk_table"`
	FKColumnName string `json:"fk_column_name"`
}

UsageSchema describes a foreign key reference to another table.

Jump to

Keyboard shortcuts

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