Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add profiling support #274

Merged
merged 13 commits into from
Sep 24, 2024
4 changes: 2 additions & 2 deletions appender.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ type Appender struct {
func NewAppenderFromConn(driverConn driver.Conn, schema, table string) (*Appender, error) {
con, ok := driverConn.(*conn)
if !ok {
return nil, getError(errAppenderInvalidCon, nil)
return nil, getError(errInvalidCon, nil)
}
if con.closed {
return nil, getError(errAppenderClosedCon, nil)
return nil, getError(errClosedCon, nil)
}

var cSchema *C.char
Expand Down
12 changes: 7 additions & 5 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,23 @@ var (
errAPI = errors.New("API error")
errVectorSize = errors.New("data chunks cannot exceed duckdb's internal vector size")

errParseDSN = errors.New("could not parse DSN for database")
errOpen = errors.New("could not open database")
errSetConfig = errors.New("could not set invalid or local option for global database config")
errParseDSN = errors.New("could not parse DSN for database")
errOpen = errors.New("could not open database")
errSetConfig = errors.New("could not set invalid or local option for global database config")
errInvalidCon = errors.New("not a DuckDB driver connection")
errClosedCon = errors.New("closed connection")

errUnsupportedMapKeyType = errors.New("MAP key type not supported")

errAppenderInvalidCon = errors.New("could not create appender: not a DuckDB driver connection")
errAppenderClosedCon = errors.New("could not create appender: appender creation on a closed connection")
errAppenderCreation = errors.New("could not create appender")
errAppenderDoubleClose = errors.New("could not close appender: already closed")
errAppenderAppendRow = errors.New("could not append row")
errAppenderAppendAfterClose = errors.New("could not append row: appender already closed")
errAppenderClose = errors.New("could not close appender")
errAppenderFlush = errors.New("could not flush appender")

errProfilingInfoEmpty = errors.New("no profiling information available for this connection")

// Errors not covered in tests.
errConnect = errors.New("could not connect to database")
errCreateConfig = errors.New("could not create config for database")
Expand Down
45 changes: 41 additions & 4 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ func TestErrNestedMap(t *testing.T) {
func TestErrAppender(t *testing.T) {
t.Parallel()

t.Run(errAppenderInvalidCon.Error(), func(t *testing.T) {
t.Run(errInvalidCon.Error(), func(t *testing.T) {
var con driver.Conn
_, err := NewAppenderFromConn(con, "", "test")
testError(t, err, errAppenderInvalidCon.Error())
testError(t, err, errInvalidCon.Error())
})

t.Run(errAppenderClosedCon.Error(), func(t *testing.T) {
t.Run(errClosedCon.Error(), func(t *testing.T) {
c, err := NewConnector("", nil)
require.NoError(t, err)

Expand All @@ -74,7 +74,7 @@ func TestErrAppender(t *testing.T) {
require.NoError(t, con.Close())

_, err = NewAppenderFromConn(con, "", "test")
testError(t, err, errAppenderClosedCon.Error())
testError(t, err, errClosedCon.Error())
require.NoError(t, c.Close())
})

Expand Down Expand Up @@ -302,6 +302,43 @@ func TestErrAPISetValue(t *testing.T) {
testError(t, err, errAPI.Error(), columnCountErrMsg)
}

func TestErrProfiling(t *testing.T) {
t.Parallel()

t.Run(errInvalidCon.Error(), func(t *testing.T) {
var con driver.Conn
_, err := GetProfilingInfo(con)
testError(t, err, errInvalidCon.Error())
})

t.Run(errClosedCon.Error(), func(t *testing.T) {
c, err := NewConnector("", nil)
require.NoError(t, err)

con, err := c.Connect(context.Background())
require.NoError(t, err)
require.NoError(t, con.Close())

_, err = GetProfilingInfo(con)
testError(t, err, errClosedCon.Error())
require.NoError(t, c.Close())
})

t.Run(errProfilingInfoEmpty.Error(), func(t *testing.T) {
c, err := NewConnector("", nil)
require.NoError(t, err)

con, err := c.Connect(context.Background())
require.NoError(t, err)

_, err = GetProfilingInfo(con)
testError(t, err, errProfilingInfoEmpty.Error())

require.NoError(t, con.Close())
require.NoError(t, c.Close())
})
}

func TestDuckDBErrors(t *testing.T) {
db := openDB(t)
defer db.Close()
Expand Down
68 changes: 68 additions & 0 deletions profiling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package duckdb

/*
#include <duckdb.h>
*/
import "C"

import (
"unsafe"
)

type ProfilingInfo struct {
Metrics map[string]string
Children []ProfilingInfo
}

func GetProfilingInfo(driverConn any) (ProfilingInfo, error) {
taniabogatsch marked this conversation as resolved.
Show resolved Hide resolved
info := ProfilingInfo{}

con, ok := driverConn.(*conn)
if !ok {
return info, getError(errInvalidCon, nil)
}
if con.closed {
return info, getError(errClosedCon, nil)
}

duckdbInfo := C.duckdb_get_profiling_info(con.duckdbCon)
if duckdbInfo == nil {
return info, getError(errProfilingInfoEmpty, nil)
}

// Recursive tree traversal.
info.getMetrics(duckdbInfo)
return info, nil
}

func (info *ProfilingInfo) getMetrics(duckdbInfo C.duckdb_profiling_info) {
m := C.duckdb_profiling_info_get_metrics(duckdbInfo)
count := C.duckdb_get_map_size(m)

for i := C.idx_t(0); i < count; i++ {
key := C.duckdb_get_map_key(m, i)
value := C.duckdb_get_map_value(m, i)

cKey := C.duckdb_get_varchar(key)
cValue := C.duckdb_get_varchar(value)

keyStr := C.GoString(cKey)
valueStr := C.GoString(cValue)

info.Metrics[keyStr] = valueStr

C.duckdb_destroy_value(&key)
C.duckdb_destroy_value(&value)
C.duckdb_free(unsafe.Pointer(cKey))
C.duckdb_free(unsafe.Pointer(cValue))
}
C.duckdb_destroy_value(&m)

childCount := C.duckdb_profiling_info_get_child_count(duckdbInfo)
for i := C.idx_t(0); i < childCount; i++ {
duckdbChildInfo := C.duckdb_profiling_info_get_child(duckdbInfo, i)
childInfo := ProfilingInfo{}
childInfo.getMetrics(duckdbChildInfo)
info.Children = append(info.Children, childInfo)
}
}
47 changes: 47 additions & 0 deletions profiling_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package duckdb

import (
"context"
"database/sql"
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestProfiling(t *testing.T) {
t.Parallel()

db, err := sql.Open("duckdb", "")
require.NoError(t, err)

con, err := db.Conn(context.Background())
require.NoError(t, err)

_, err = con.ExecContext(context.Background(), `PRAGMA enable_profiling = 'no_output'`)
require.NoError(t, err)

_, err = con.ExecContext(context.Background(), `PRAGMA profiling_mode = 'detailed'`)
require.NoError(t, err)

res, err := con.QueryContext(context.Background(), "SELECT range AS i FROM range(100) ORDER BY i")
require.NoError(t, err)

var info ProfilingInfo
err = con.Raw(func(driverCon any) error {
info, err = GetProfilingInfo(driverCon)
return err
})
require.NoError(t, err)

require.NoError(t, res.Close())
require.NoError(t, con.Close())
require.NoError(t, db.Close())

// Verify the metrics.
// TODO: currently failing due to C API bug.
fmt.Println(info) // Dummy print to use variable.
// require.NotEmpty(t, info.Metrics, "metrics must not be empty")
// require.NotEmpty(t, info.Children, "children must not be empty")
// require.NotEmpty(t, info.Children[0].Metrics, "child metrics must not be empty")
}