Skip to content

Use chdb stable ABI v2 for standard golang err handling #5

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

Merged
merged 7 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 37 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,11 @@
./chdb-go --path /tmp/chdb # interactive persistent mode
```

3. Shortcuts

- `\l` to list databases;
- `\dt dbname` to list tables in database;

```bash
chdb-io/chdb-go [main] » ./chdb-go
Enter your SQL commands; type 'exit' to quit.
:) CREATE DATABASE IF NOT EXISTS testdb;

:) \l
┏━━━━━━━━━━━━━━━━━━━━┓
┃ name ┃
┡━━━━━━━━━━━━━━━━━━━━┩
│ INFORMATION_SCHEMA │
├────────────────────┤
│ _local │
├────────────────────┤
│ information_schema │
├────────────────────┤
│ system │
├────────────────────┤
│ testdb │
└────────────────────┘

:) CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree()
:-] ORDER BY id;

:) \dt testdb
┏━━━━━━━━━━━┓
┃ name ┃
┡━━━━━━━━━━━┩
│ testtable │
└───────────┘

```

Expand All @@ -72,34 +43,50 @@ Enter your SQL commands; type 'exit' to quit.
package main

import (
"fmt"
"github.com/chdb-io/chdb-go/chdb"
"fmt"
"os"
"path/filepath"

"github.com/chdb-io/chdb-go/chdb"
)

func main() {
// Stateless Query (ephemeral)
result := chdb.Query("SELECT version()", "CSV")
fmt.Println(result)

// Stateful Query (persistent)
session, _ := NewSession(path)
defer session.Cleanup()

session.Query("CREATE DATABASE IF NOT EXISTS testdb; " +
"CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree() ORDER BY id;")

session.Query("USE testdb; INSERT INTO testtable VALUES (1), (2), (3);")

ret := session.Query("SELECT * FROM testtable;")
fmt.Println(ret)
// Stateless Query (ephemeral)
result, err := chdb.Query("SELECT version()", "CSV")
if err != nil {
fmt.Println(err)
}
fmt.Println(result)

tmp_path := filepath.Join(os.TempDir(), "chdb_test")
defer os.RemoveAll(tmp_path)
// Stateful Query (persistent)
session, _ := chdb.NewSession(tmp_path)
defer session.Cleanup()

_, err = session.Query("CREATE DATABASE IF NOT EXISTS testdb; " +
"CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree() ORDER BY id;")
if err != nil {
fmt.Println(err)
return
}

_, err = session.Query("USE testdb; INSERT INTO testtable VALUES (1), (2), (3);")
if err != nil {
fmt.Println(err)
return
}

ret, err := session.Query("SELECT * FROM testtable;")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(ret)
}
}
```

### Golang API docs

- See [lowApi.md](lowApi.md) for the low level APIs.
- See [chdb.md](chdb.md) for high level APIs.

### Thanks

- cli implementation is based on [clickhouse-cli](https://github.com/memlimit/clickhouse-cli)
7 changes: 5 additions & 2 deletions chdb/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func init() {
sql.Register("chdb", Driver{})
}

type queryHandle func(string, ...string) *chdbstable.LocalResult
type queryHandle func(string, ...string) (*chdbstable.LocalResult, error)

type connector struct {
udfPath string
Expand Down Expand Up @@ -127,7 +127,10 @@ func (c *conn) Query(query string, values []driver.Value) (driver.Rows, error) {
}

func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
result := c.QueryFun(query, "Arrow", c.udfPath)
result, err := c.QueryFun(query, "Arrow", c.udfPath)
if err != nil {
return nil, err
}
buf := result.Buf()
if buf == nil {
return nil, fmt.Errorf("result is nil")
Expand Down
5 changes: 4 additions & 1 deletion chdb/driver/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ func TestDbWithSession(t *testing.T) {

session.Query("USE testdb; INSERT INTO testtable VALUES (1), (2), (3);")

ret := session.Query("SELECT * FROM testtable;")
ret, err := session.Query("SELECT * FROM testtable;")
if err != nil {
t.Fatalf("Query fail, err: %s", err)
}
if string(ret.Buf()) != "1\n2\n3\n" {
t.Errorf("Query result should be 1\n2\n3\n, got %s", string(ret.Buf()))
}
Expand Down
2 changes: 1 addition & 1 deletion chdb/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewSession(paths ...string) (*Session, error) {
}

// Query calls queryToBuffer with a default output format of "CSV" if not provided.
func (s *Session) Query(queryStr string, outputFormats ...string) *chdbstable.LocalResult {
func (s *Session) Query(queryStr string, outputFormats ...string) (result *chdbstable.LocalResult, err error) {
outputFormat := "CSV" // Default value
if len(outputFormats) > 0 {
outputFormat = outputFormats[0]
Expand Down
5 changes: 4 additions & 1 deletion chdb/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ func TestQuery(t *testing.T) {

session.Query("USE testdb; INSERT INTO testtable VALUES (1), (2), (3);")

ret := session.Query("SELECT * FROM testtable;")
ret, err := session.Query("SELECT * FROM testtable;")
if err != nil {
t.Errorf("Query failed: %s", err)
}
if string(ret.Buf()) != "1\n2\n3\n" {
t.Errorf("Query result should be 1\n2\n3\n, got %s", string(ret.Buf()))
}
Expand Down
4 changes: 2 additions & 2 deletions chdb/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

// Query calls queryToBuffer with a default output format of "CSV" if not provided.
func Query(queryStr string, outputFormats ...string) *chdbstable.LocalResult {
func Query(queryStr string, outputFormats ...string) (result *chdbstable.LocalResult, err error) {
outputFormat := "CSV" // Default value
if len(outputFormats) > 0 {
outputFormat = outputFormats[0]
Expand All @@ -14,7 +14,7 @@ func Query(queryStr string, outputFormats ...string) *chdbstable.LocalResult {
}

// queryToBuffer constructs the arguments for QueryStable and calls it.
func queryToBuffer(queryStr, outputFormat, path, udfPath string) *chdbstable.LocalResult {
func queryToBuffer(queryStr, outputFormat, path, udfPath string) (result *chdbstable.LocalResult, err error) {
argv := []string{"clickhouse", "--multiquery"}

// Handle output format
Expand Down
61 changes: 42 additions & 19 deletions chdb/wrapper_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package chdb

import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)

func TestQueryToBuffer(t *testing.T) {
// Create a temporary directory
tempDir, err := ioutil.TempDir("", "example")
if err != nil {
t.Fatalf("Failed to create temporary directory: %v", err)
}
tempDir := filepath.Join(os.TempDir(), "chdb_test")
defer os.RemoveAll(tempDir)

// Define test cases
Expand All @@ -21,6 +18,7 @@ func TestQueryToBuffer(t *testing.T) {
outputFormat string
path string
udfPath string
expectedErrMsg string
expectedResult string
}{
{
Expand All @@ -29,6 +27,7 @@ func TestQueryToBuffer(t *testing.T) {
outputFormat: "CSV",
path: "",
udfPath: "",
expectedErrMsg: "",
expectedResult: "123\n",
},
// Session
Expand All @@ -39,35 +38,59 @@ func TestQueryToBuffer(t *testing.T) {
outputFormat: "CSV",
path: tempDir,
udfPath: "",
expectedErrMsg: "",
expectedResult: "",
},
// {
// name: "Session Query 2",
// queryStr: "USE testdb; INSERT INTO testtable VALUES (1), (2), (3);",
// outputFormat: "CSV",
// path: tempDir,
// udfPath: "",
// expectedErrMsg: "",
// expectedResult: "",
// },
// {
// name: "Session Query 3",
// queryStr: "SELECT * FROM testtable;",
// outputFormat: "CSV",
// path: tempDir,
// udfPath: "",
// expectedErrMsg: "",
// expectedResult: "1\n2\n3\n",
// },
{
name: "Session Query 2",
queryStr: "USE testdb; INSERT INTO testtable VALUES (1), (2), (3);",
name: "Error Query",
queryStr: "SELECT * FROM nonexist; ",
outputFormat: "CSV",
path: tempDir,
udfPath: "",
expectedErrMsg: "Code: 60. DB::Exception: Table _local.nonexist does not exist. (UNKNOWN_TABLE)",
expectedResult: "",
},
{
name: "Session Query 3",
queryStr: "SELECT * FROM testtable;",
outputFormat: "CSV",
path: tempDir,
udfPath: "",
expectedResult: "1\n2\n3\n",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Call queryToBuffer
result := queryToBuffer(tc.queryStr, tc.outputFormat, tc.path, tc.udfPath)
result, err := queryToBuffer(tc.queryStr, tc.outputFormat, tc.path, tc.udfPath)

// Verify
if string(result.Buf()) != tc.expectedResult {
t.Errorf("%v queryToBuffer() with queryStr %v, outputFormat %v, path %v, udfPath %v, expect result: %v, got result: %v",
tc.name, tc.queryStr, tc.outputFormat, tc.path, tc.udfPath, tc.expectedResult, string(result.Buf()))
if tc.expectedErrMsg != "" {
if err == nil {
t.Errorf("%v queryToBuffer() with queryStr %v, outputFormat %v, path %v, udfPath %v, expect error message: %v, got no error",
tc.name, tc.queryStr, tc.outputFormat, tc.path, tc.udfPath, tc.expectedErrMsg)
} else {
if err.Error() != tc.expectedErrMsg {
t.Errorf("%v queryToBuffer() with queryStr %v, outputFormat %v, path %v, udfPath %v, expect error message: %v, got error message: %v",
tc.name, tc.queryStr, tc.outputFormat, tc.path, tc.udfPath, tc.expectedErrMsg, err.Error())
}
}
} else {
if string(result.Buf()) != tc.expectedResult {
t.Errorf("%v queryToBuffer() with queryStr %v, outputFormat %v, path %v, udfPath %v, expect result: %v, got result: %v",
tc.name, tc.queryStr, tc.outputFormat, tc.path, tc.udfPath, tc.expectedResult, string(result.Buf()))
}
}
})
}
Expand Down
40 changes: 31 additions & 9 deletions chdbstable/chdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,62 @@ package chdbstable
*/
import "C"
import (
"errors"
"runtime"
"unsafe"
)

// LocalResult mirrors the C struct local_result in Go.
// ChdbError is returned when the C function returns an error.
type ChdbError struct {
msg string
}

func (e *ChdbError) Error() string {
return e.msg
}

// ErrNilResult is returned when the C function returns a nil pointer.
var ErrNilResult = errors.New("chDB C function returned nil pointer")

// LocalResult mirrors the C struct local_result_v2 in Go.
type LocalResult struct {
cResult *C.struct_local_result
cResult *C.struct_local_result_v2
}

// newLocalResult creates a new LocalResult and sets a finalizer to free C memory.
func newLocalResult(cResult *C.struct_local_result) *LocalResult {
func newLocalResult(cResult *C.struct_local_result_v2) *LocalResult {
result := &LocalResult{cResult: cResult}
runtime.SetFinalizer(result, freeLocalResult)
return result
}

// freeLocalResult is called by the garbage collector.
func freeLocalResult(result *LocalResult) {
C.free_result(result.cResult)
C.free_result_v2(result.cResult)
}

// QueryStable calls the C function query_stable.
func QueryStable(argc int, argv []string) *LocalResult {
// QueryStable calls the C function query_stable_v2.
func QueryStable(argc int, argv []string) (result *LocalResult, err error) {
cArgv := make([]*C.char, len(argv))
for i, s := range argv {
cArgv[i] = C.CString(s)
defer C.free(unsafe.Pointer(cArgv[i]))
}

cResult := C.query_stable(C.int(argc), &cArgv[0])
return newLocalResult(cResult)
cResult := C.query_stable_v2(C.int(argc), &cArgv[0])
if cResult == nil {
// According to the C ABI of chDB v1.2.0, the C function query_stable_v2
// returns nil if the query returns no data. This is not an error. We
// will change this behavior in the future.
return newLocalResult(cResult), nil
}
if cResult.error_message != nil {
return nil, &ChdbError{msg: C.GoString(cResult.error_message)}
}
return newLocalResult(cResult), nil
}

// Accessor methods to access fields of the local_result struct.
// Accessor methods to access fields of the local_result_v2 struct.
func (r *LocalResult) Buf() []byte {
if r.cResult == nil {
return nil
Expand Down
16 changes: 15 additions & 1 deletion chdbstable/chdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extern "C" {
#endif

#define CHDB_EXPORT __attribute__((visibility("default")))
struct CHDB_EXPORT local_result
struct local_result
{
char * buf;
size_t len;
Expand All @@ -20,9 +20,23 @@ struct CHDB_EXPORT local_result
uint64_t bytes_read;
};

struct local_result_v2
{
char * buf;
size_t len;
void * _vec; // std::vector<char> *, for freeing
double elapsed;
uint64_t rows_read;
uint64_t bytes_read;
char * error_message;
};

CHDB_EXPORT struct local_result * query_stable(int argc, char ** argv);
CHDB_EXPORT void free_result(struct local_result * result);

CHDB_EXPORT struct local_result_v2 * query_stable_v2(int argc, char ** argv);
CHDB_EXPORT void free_result_v2(struct local_result_v2 * result);

#ifdef __cplusplus
}
#endif
Loading