Go SQL driver for DuckDB
The DuckDB driver conforms to the built-in database/sql
interface.
go get github.com/marcboeker/go-duckdb
go-duckdb
uses CGO
to make calls to DuckDB. You must build your binaries with CGO_ENABLED=1
.
go-duckdb
hooks into the database/sql
interface provided by the Go stdlib
. To open a connection, simply specify the driver type as duckdb
.
db, err := sql.Open("duckdb", "")
if err != nil {
...
}
defer db.Close()
This creates an in-memory instance of DuckDB. To open a persistent database, you need to specify a filepath to the database file. If the file does not exist, then DuckDB creates it.
db, err := sql.Open("duckdb", "/path/to/foo.db")
if err != nil {
...
}
defer db.Close()
If you want to set specific config options for DuckDB, you can add them as query style parameters in the form of name=value
pairs to the DSN.
db, err := sql.Open("duckdb", "/path/to/foo.db?access_mode=read_only&threads=4")
if err != nil {
...
}
defer db.Close()
Alternatively, you can use sql.OpenDB. That way, you can perform initialization steps in a callback function before opening the database.
Here's an example that installs and loads the JSON extension when opening a database with sql.OpenDB(connector)
.
connector, err := duckdb.NewConnector("/path/to/foo.db?access_mode=read_only&threads=4", func(execer driver.ExecerContext) error {
bootQueries := []string{
"INSTALL 'json'",
"LOAD 'json'",
}
for _, query := range bootQueries {
_, err = execer.ExecContext(context.Background(), query, nil)
if err != nil {
...
}
}
return nil
})
if err != nil {
...
}
db := sql.OpenDB(connector)
defer db.Close()
Please refer to the database/sql documentation for further usage instructions.
DuckDB lives in-process. Therefore, all its memory lives in the driver. All allocations live in the host process, which
is the Go application. Especially for long-running applications, it is crucial to call the corresponding Close
-functions as specified
in database/sql. The following is a list of examples.
db, err := sql.Open("duckdb", "")
defer db.Close()
conn, err := db.Conn(context.Background())
defer conn.Close()
rows, err := conn.QueryContext(context.Background(), "SELECT 42")
// alternatively, rows.Next() has to return false
rows.Close()
appender, err := NewAppenderFromConn(conn, "", "test")
defer appender.Close()
// if not passed to sql.OpenDB
connector, err := NewConnector("", nil)
defer connector.Close()
If you want to use the DuckDB Appender API, you can obtain a new Appender
by passing a DuckDB connection to NewAppenderFromConn()
.
connector, err := duckdb.NewConnector("test.db", nil)
if err != nil {
...
}
defer connector.Close()
conn, err := connector.Connect(context.Background())
if err != nil {
...
}
defer conn.Close()
// obtain an appender from the connection
// NOTE: the table 'test_tbl' must exist in test.db
appender, err := NewAppenderFromConn(conn, "", "test_tbl")
if err != nil {
...
}
defer appender.Close()
err = appender.AppendRow(...)
if err != nil {
...
}
If you want to use the DuckDB Arrow Interface, you can obtain a new Arrow
by passing a DuckDB connection to NewArrowFromConn()
.
connector, err := duckdb.NewConnector("", nil)
if err != nil {
...
}
defer connector.Close()
conn, err := connector.Connect(context.Background())
if err != nil {
...
}
defer conn.Close()
// obtain the Arrow from the connection
arrow, err := duckdb.NewArrowFromConn(conn)
if err != nil w
...
}
rdr, err := arrow.QueryContext(context.Background(), "SELECT * FROM generate_series(1, 10)")
if err != nil {
...
}
defer rdr.Release()
for rdr.Next() {
// process records
}
If you want to vendor a module containing go-duckdb
, please use modvendor
to include the missing header files and libraries.
See issue #174 for more details.
go install github.com/goware/modvendor@latest
go mod vendor
modvendor -copy="**/*.a **/*.h" -v
Now you can build your module as usual.
By default, go-duckdb
statically links DuckDB into your binary. Statically linking DuckDB adds around 30 MB to your binary size. On Linux (Intel) and macOS (Intel and ARM), go-duckdb
bundles pre-compiled static libraries for fast builds.
Alternatively, you can dynamically link DuckDB by passing -tags=duckdb_use_lib
to go build
. You must have a copy of libduckdb
available on your system (.so
on Linux or .dylib
on macOS), which you can download from the DuckDB releases page. For example:
# On Linux
CGO_ENABLED=1 CGO_LDFLAGS="-L/path/to/libs" go build -tags=duckdb_use_lib main.go
LD_LIBRARY_PATH=/path/to/libs ./main
# On macOS
CGO_ENABLED=1 CGO_LDFLAGS="-L/path/to/libs" go build -tags=duckdb_use_lib main.go
DYLD_LIBRARY_PATH=/path/to/libs ./main
TIMESTAMP vs. TIMESTAMP_TZ
In the C API, DuckDB stores both TIMESTAMP
and TIMESTAMP_TZ
as duckdb_timestamp
, which holds the number of
microseconds elapsed since January 1, 1970 UTC (i.e., an instant without offset information).
When passing a time.Time
to go-duckdb, go-duckdb transforms it to an instant with UnixMicro()
,
even when using TIMESTAMP_TZ
. Later, scanning either type of value returns an instant, as SQL types do not model
time zone information for individual values.