Skip to content

Commit 2585b57

Browse files
committed
Basic chdb cli works
1 parent 4a073a6 commit 2585b57

File tree

7 files changed

+1005
-0
lines changed

7 files changed

+1005
-0
lines changed

Makefile

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.PHONY: update_libchdb all test clean
2+
3+
update_libchdb:
4+
# ./update_libchdb.sh
5+
@echo "All the tests an main.go will search libchdb.so in the Current Working Directory"
6+
@echo "We perfer to put libchdb.so in /usr/local/lib, so you can run"
7+
@echo "'sudo cp -a libchdb.so /usr/local/lib' or 'make install' to do it"
8+
@echo "You can also put it in other places, but you need to set LD_LIBRARY_PATH on Linux or DYLD_LIBRARY_PATH on macOS"
9+
10+
install: update_libchdb
11+
chmod +x libchdb.so
12+
sudo cp -a libchdb.so /usr/local/lib
13+
# if on Linux run `sudo ldconfig` to update the cache
14+
# if on macOS run `sudo update_dyld_shared_cache` to update the cache
15+
if [[ "$$OSTYPE" == "linux-gnu" ]]; then sudo ldconfig; fi
16+
17+
test:
18+
CGO_ENABLED=1 go test -v -coverprofile=coverage.out ./...
19+
20+
run:
21+
CGO_ENABLED=1 go run main.go
22+
23+
build:
24+
CGO_ENABLED=1 go build -o chdb-cli main.go

chdb/session.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package chdb
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/chdb-io/chdb-go/chdbstable"
9+
)
10+
11+
type Session struct {
12+
path string
13+
isTemp bool
14+
}
15+
16+
// NewSession creates a new session with the given path.
17+
// If path is empty, a temporary directory is created.
18+
// Note: The temporary directory is removed when Close is called.
19+
func NewSession(paths ...string) (*Session, error) {
20+
path := ""
21+
if len(paths) > 0 {
22+
path = paths[0]
23+
}
24+
25+
if path == "" {
26+
// Create a temporary directory
27+
tempDir, err := ioutil.TempDir("", "chdb_")
28+
if err != nil {
29+
return nil, err
30+
}
31+
path = tempDir
32+
return &Session{path: path, isTemp: true}, nil
33+
}
34+
35+
return &Session{path: path, isTemp: false}, nil
36+
}
37+
38+
// Query calls queryToBuffer with a default output format of "CSV" if not provided.
39+
func (s *Session) Query(queryStr string, outputFormats ...string) *chdbstable.LocalResult {
40+
outputFormat := "CSV" // Default value
41+
if len(outputFormats) > 0 {
42+
outputFormat = outputFormats[0]
43+
}
44+
return queryToBuffer(queryStr, outputFormat, s.path, "")
45+
}
46+
47+
// Close closes the session and removes the temporary directory
48+
// temporary directory is created when NewSession was called with an empty path.
49+
func (s *Session) Close() {
50+
// Remove the temporary directory if it starts with "chdb_"
51+
if s.isTemp && filepath.Base(s.path)[:5] == "chdb_" {
52+
s.Cleanup()
53+
}
54+
}
55+
56+
// Cleanup closes the session and removes the directory.
57+
func (s *Session) Cleanup() {
58+
// Remove the session directory, no matter if it is temporary or not
59+
_ = os.RemoveAll(s.path)
60+
}
61+
62+
// Path returns the path of the session.
63+
func (s *Session) Path() string {
64+
return s.path
65+
}
66+
67+
// IsTemp returns whether the session is temporary.
68+
func (s *Session) IsTemp() bool {
69+
return s.isTemp
70+
}

chdb/session_test.go

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package chdb
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
// TestNewSession tests the creation of a new session.
10+
func TestNewSession(t *testing.T) {
11+
session, err := NewSession()
12+
if err != nil {
13+
t.Fatalf("Failed to create new session: %s", err)
14+
}
15+
defer session.Cleanup()
16+
17+
// Check if the session directory exists
18+
if _, err := os.Stat(session.Path()); os.IsNotExist(err) {
19+
t.Errorf("Session directory does not exist: %s", session.Path())
20+
}
21+
22+
// Check if the session is temporary
23+
if !session.IsTemp() {
24+
t.Errorf("Expected session to be temporary")
25+
}
26+
}
27+
28+
// TestSessionClose tests the Close method of the session.
29+
func TestSessionClose(t *testing.T) {
30+
session, _ := NewSession()
31+
defer session.Cleanup() // Cleanup in case Close fails
32+
33+
// Close the session
34+
session.Close()
35+
36+
// Check if the session directory has been removed
37+
if _, err := os.Stat(session.Path()); !os.IsNotExist(err) {
38+
t.Errorf("Session directory should be removed after Close: %s", session.Path())
39+
}
40+
}
41+
42+
// TestSessionCleanup tests the Cleanup method of the session.
43+
func TestSessionCleanup(t *testing.T) {
44+
session, _ := NewSession()
45+
46+
// Cleanup the session
47+
session.Cleanup()
48+
49+
// Check if the session directory has been removed
50+
if _, err := os.Stat(session.Path()); !os.IsNotExist(err) {
51+
t.Errorf("Session directory should be removed after Cleanup: %s", session.Path())
52+
}
53+
}
54+
55+
// TestQuery tests the Query method of the session.
56+
func TestQuery(t *testing.T) {
57+
path := filepath.Join(os.TempDir(), "chdb_test")
58+
defer os.RemoveAll(path)
59+
session, _ := NewSession(path)
60+
defer session.Cleanup()
61+
62+
session.Query("CREATE DATABASE IF NOT EXISTS testdb; " +
63+
"CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree() ORDER BY id;")
64+
65+
session.Query("USE testdb; INSERT INTO testtable VALUES (1), (2), (3);")
66+
67+
ret := session.Query("SELECT * FROM testtable;")
68+
if string(ret.Buf()) != "1\n2\n3\n" {
69+
t.Errorf("Query result should be 1\n2\n3\n, got %s", string(ret.Buf()))
70+
}
71+
}
72+
73+
func TestSessionPathAndIsTemp(t *testing.T) {
74+
// Create a new session and check its Path and IsTemp
75+
session, _ := NewSession()
76+
defer session.Cleanup()
77+
78+
if session.Path() == "" {
79+
t.Errorf("Session path should not be empty")
80+
}
81+
82+
if !session.IsTemp() {
83+
t.Errorf("Session should be temporary")
84+
}
85+
86+
// Create a new session with a specific path and check its Path and IsTemp
87+
path := filepath.Join(os.TempDir(), "chdb_test2")
88+
defer os.RemoveAll(path)
89+
session, _ = NewSession(path)
90+
defer session.Cleanup()
91+
92+
if session.Path() != path {
93+
t.Errorf("Session path should be %s, got %s", path, session.Path())
94+
}
95+
96+
if session.IsTemp() {
97+
t.Errorf("Session should not be temporary")
98+
}
99+
}

chdb/wrapper.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package chdb
2+
3+
import (
4+
"github.com/chdb-io/chdb-go/chdbstable"
5+
)
6+
7+
// Query calls queryToBuffer with a default output format of "CSV" if not provided.
8+
func Query(queryStr string, outputFormats ...string) *chdbstable.LocalResult {
9+
outputFormat := "CSV" // Default value
10+
if len(outputFormats) > 0 {
11+
outputFormat = outputFormats[0]
12+
}
13+
return queryToBuffer(queryStr, outputFormat, "", "")
14+
}
15+
16+
// queryToBuffer constructs the arguments for QueryStable and calls it.
17+
func queryToBuffer(queryStr, outputFormat, path, udfPath string) *chdbstable.LocalResult {
18+
argv := []string{"clickhouse", "--multiquery"}
19+
20+
// Handle output format
21+
if outputFormat == "Debug" || outputFormat == "debug" {
22+
argv = append(argv, "--verbose", "--log-level=trace", "--output-format=CSV")
23+
} else {
24+
argv = append(argv, "--output-format="+outputFormat)
25+
}
26+
27+
// Handle path
28+
if path != "" {
29+
argv = append(argv, "--path="+path)
30+
}
31+
32+
// Add query string
33+
argv = append(argv, "--query="+queryStr)
34+
35+
// Handle user-defined functions path
36+
if udfPath != "" {
37+
argv = append(argv, "--", "--user_scripts_path="+udfPath, "--user_defined_executable_functions_config="+udfPath+"/*.xml")
38+
}
39+
40+
// Call QueryStable with the constructed arguments
41+
return chdbstable.QueryStable(len(argv), argv)
42+
}

chdb/wrapper_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package chdb
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"testing"
7+
)
8+
9+
func TestQueryToBuffer(t *testing.T) {
10+
// Create a temporary directory
11+
tempDir, err := ioutil.TempDir("", "example")
12+
if err != nil {
13+
t.Fatalf("Failed to create temporary directory: %v", err)
14+
}
15+
defer os.RemoveAll(tempDir)
16+
17+
// Define test cases
18+
testCases := []struct {
19+
name string
20+
queryStr string
21+
outputFormat string
22+
path string
23+
udfPath string
24+
expectedResult string
25+
}{
26+
{
27+
name: "Basic Query",
28+
queryStr: "SELECT 123",
29+
outputFormat: "CSV",
30+
path: "",
31+
udfPath: "",
32+
expectedResult: "123\n",
33+
},
34+
// Session
35+
{
36+
name: "Session Query 1",
37+
queryStr: "CREATE DATABASE IF NOT EXISTS testdb; "+
38+
"CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree() ORDER BY id;",
39+
outputFormat: "CSV",
40+
path: tempDir,
41+
udfPath: "",
42+
expectedResult: "",
43+
},
44+
{
45+
name: "Session Query 2",
46+
queryStr: "USE testdb; INSERT INTO testtable VALUES (1), (2), (3);",
47+
outputFormat: "CSV",
48+
path: tempDir,
49+
udfPath: "",
50+
expectedResult: "",
51+
},
52+
{
53+
name: "Session Query 3",
54+
queryStr: "SELECT * FROM testtable;",
55+
outputFormat: "CSV",
56+
path: tempDir,
57+
udfPath: "",
58+
expectedResult: "1\n2\n3\n",
59+
},
60+
}
61+
62+
for _, tc := range testCases {
63+
t.Run(tc.name, func(t *testing.T) {
64+
// Call queryToBuffer
65+
result := queryToBuffer(tc.queryStr, tc.outputFormat, tc.path, tc.udfPath)
66+
67+
// Verify
68+
if string(result.Buf()) != tc.expectedResult {
69+
t.Errorf("%v queryToBuffer() with queryStr %v, outputFormat %v, path %v, udfPath %v, expect result: %v, got result: %v",
70+
tc.name, tc.queryStr, tc.outputFormat, tc.path, tc.udfPath, tc.expectedResult, string(result.Buf()))
71+
}
72+
})
73+
}
74+
}

0 commit comments

Comments
 (0)