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

Implement chdb v3.0.0 with pureGo instead of cGo #18

Merged
merged 22 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/chdb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
run: ./chdb-go "SELECT 12345"

build_mac:
runs-on: macos-12
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- name: Fetch library
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ func main() {
fmt.Println(result)

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

_, err = session.Query("CREATE DATABASE IF NOT EXISTS testdb; " +
Expand Down
62 changes: 62 additions & 0 deletions chdb-purego/binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package chdbpurego

import (
"os"
"os/exec"

"github.com/ebitengine/purego"
)

func findLibrary() string {
// Env var
if envPath := os.Getenv("CHDB_LIB_PATH"); envPath != "" {
return envPath
}

// ldconfig with Linux
if path, err := exec.LookPath("libchdb.so"); err == nil {
return path
}

// default path
commonPaths := []string{
"/usr/local/lib/libchdb.so",
"/opt/homebrew/lib/libchdb.dylib",
}

for _, p := range commonPaths {
if _, err := os.Stat(p); err == nil {
return p
}
}

//should be an error ?
return "libchdb.so"
}

var (
queryStable func(argc int, argv []string) *local_result
freeResult func(result *local_result)
queryStableV2 func(argc int, argv []string) *local_result_v2
freeResultV2 func(result *local_result_v2)
connectChdb func(argc int, argv []string) **chdb_conn
closeConn func(conn **chdb_conn)
queryConn func(conn *chdb_conn, query string, format string) *local_result_v2
)

func init() {
path := findLibrary()
libchdb, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
panic(err)
}
purego.RegisterLibFunc(&queryStable, libchdb, "query_stable")
purego.RegisterLibFunc(&freeResult, libchdb, "free_result")
purego.RegisterLibFunc(&queryStableV2, libchdb, "query_stable_v2")

purego.RegisterLibFunc(&freeResultV2, libchdb, "free_result_v2")
purego.RegisterLibFunc(&connectChdb, libchdb, "connect_chdb")
purego.RegisterLibFunc(&closeConn, libchdb, "close_conn")
purego.RegisterLibFunc(&queryConn, libchdb, "query_conn")

}
194 changes: 194 additions & 0 deletions chdb-purego/chdb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package chdbpurego

import (
"errors"
"fmt"
"unsafe"
)

type result struct {
localResv2 *local_result_v2
}

func newChdbResult(cRes *local_result_v2) ChdbResult {
res := &result{
localResv2: cRes,
}
// runtime.SetFinalizer(res, res.Free)
return res

}

// Buf implements ChdbResult.
func (c *result) Buf() []byte {
if c.localResv2 != nil {
if c.localResv2.buf != nil && c.localResv2.len > 0 {
return unsafe.Slice(c.localResv2.buf, c.localResv2.len)
}
}
return nil
}

// BytesRead implements ChdbResult.
func (c *result) BytesRead() uint64 {
if c.localResv2 != nil {
return c.localResv2.bytes_read
}
return 0
}

// Elapsed implements ChdbResult.
func (c *result) Elapsed() float64 {
if c.localResv2 != nil {
return c.localResv2.elapsed
}
return 0
}

// Error implements ChdbResult.
func (c *result) Error() error {
if c.localResv2 != nil {
if c.localResv2.error_message != nil {
return errors.New(ptrToGoString(c.localResv2.error_message))
}
}
return nil
}

// Free implements ChdbResult.
func (c *result) Free() {
if c.localResv2 != nil {
freeResultV2(c.localResv2)
c.localResv2 = nil
}

}

// Len implements ChdbResult.
func (c *result) Len() int {
if c.localResv2 != nil {
return int(c.localResv2.len)
}
return 0
}

// RowsRead implements ChdbResult.
func (c *result) RowsRead() uint64 {
if c.localResv2 != nil {
return c.localResv2.rows_read
}
return 0
}

// String implements ChdbResult.
func (c *result) String() string {
ret := c.Buf()
if ret == nil {
return ""
}
return string(ret)
}

type connection struct {
conn **chdb_conn
}

func newChdbConn(conn **chdb_conn) ChdbConn {
c := &connection{
conn: conn,
}
// runtime.SetFinalizer(c, c.Close)
return c
}

// Close implements ChdbConn.
func (c *connection) Close() {
if c.conn != nil {
closeConn(c.conn)
}
}

// Query implements ChdbConn.
func (c *connection) Query(queryStr string, formatStr string) (result ChdbResult, err error) {

if c.conn == nil {
return nil, fmt.Errorf("invalid connection")
}

rawConn := *c.conn

res := queryConn(rawConn, queryStr, formatStr)
if res == 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 newChdbResult(res), nil
}
if res.error_message != nil {
return nil, errors.New(ptrToGoString(res.error_message))
}

return newChdbResult(res), nil
}

func (c *connection) Ready() bool {
if c.conn != nil {
deref := *c.conn
if deref != nil {
return deref.connected
}
}
return false
}

// RawQuery will execute the given clickouse query without using any session.
func RawQuery(argc int, argv []string) (result ChdbResult, err error) {
agoncear-mwb marked this conversation as resolved.
Show resolved Hide resolved
res := queryStableV2(argc, argv)
if res == 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 newChdbResult(res), nil
}
if res.error_message != nil {
return nil, errors.New(ptrToGoString(res.error_message))
}

return newChdbResult(res), nil
}

// Session will keep the state of query.
// If path is None, it will create a temporary directory and use it as the database path
// and the temporary directory will be removed when the session is closed.
// You can also pass in a path to create a database at that path where will keep your data.
//
// You can also use a connection string to pass in the path and other parameters.
// Examples:
// - ":memory:" (for in-memory database)
// - "test.db" (for relative path)
// - "file:test.db" (same as above)
// - "/path/to/test.db" (for absolute path)
// - "file:/path/to/test.db" (same as above)
// - "file:test.db?param1=value1&param2=value2" (for relative path with query params)
// - "file::memory:?verbose&log-level=test" (for in-memory database with query params)
// - "///path/to/test.db?param1=value1&param2=value2" (for absolute path)
//
// Connection string args handling:
//
// Connection string can contain query params like "file:test.db?param1=value1&param2=value2"
// "param1=value1" will be passed to ClickHouse engine as start up args.
//
// For more details, see `clickhouse local --help --verbose`
// Some special args handling:
// - "mode=ro" would be "--readonly=1" for clickhouse (read-only mode)
//
// Important:
// - There can be only one session at a time. If you want to create a new session, you need to close the existing one.
// - Creating a new session will close the existing one.
func NewConnection(argc int, argv []string) (ChdbConn, error) {
conn := connectChdb(argc, argv)
if conn == nil {
return nil, fmt.Errorf("could not create a chdb connection")
}
return newChdbConn(conn), nil
}
21 changes: 21 additions & 0 deletions chdb-purego/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package chdbpurego

import (
"unsafe"
)

func ptrToGoString(ptr *byte) string {
if ptr == nil {
return ""
}

var length int
for {
if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(length))) == 0 {
break
}
length++
}

return string(unsafe.Slice(ptr, length))
}
60 changes: 60 additions & 0 deletions chdb-purego/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package chdbpurego

import "unsafe"

// old local result struct. for reference:
// https://github.com/chdb-io/chdb/blob/main/programs/local/chdb.h#L29
type local_result struct {
buf *byte
len uintptr
_vec unsafe.Pointer
elapsed float64
rows_read uint64
bytes_read uint64
}

// new local result struct. for reference: https://github.com/chdb-io/chdb/blob/main/programs/local/chdb.h#L40
type local_result_v2 struct {
buf *byte
len uintptr
_vec unsafe.Pointer
elapsed float64
rows_read uint64
bytes_read uint64
error_message *byte
}

// clickhouse background server connection.for reference: https://github.com/chdb-io/chdb/blob/main/programs/local/chdb.h#L82
type chdb_conn struct {
server unsafe.Pointer
connected bool
queue unsafe.Pointer
}

type ChdbResult interface {
// Raw bytes result buffer, used for reading the result of clickhouse query
Buf() []byte
// String rapresentation of the the buffer
String() string
// Lenght in bytes of the buffer
Len() int
// Number of seconds elapsed for the query execution
Elapsed() float64
// Amount of rows returned by the query
RowsRead() uint64
// Amount of bytes returned by the query
BytesRead() uint64
// If the query had any error during execution, here you can retrieve the cause.
Error() error
// Free the query result and all the allocated memory
Free()
}

type ChdbConn interface {
//Query executes the given queryStr in the underlying clickhouse connection, and output the result in the given formatStr
Query(queryStr string, formatStr string) (result ChdbResult, err error)
//Ready returns a boolean indicating if the connections is successfully established.
Ready() bool
//Close the connection and free the underlying allocated memory
Close()
}
Loading