Skip to content

Commit 3cb97aa

Browse files
authored
Merge pull request #21 from chdb-io/libchdb3
Add NewConnectionFromConnString and fix session issues
2 parents d4c774f + 114587a commit 3cb97aa

File tree

8 files changed

+343
-16
lines changed

8 files changed

+343
-16
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ libchdb.tar.gz
1616

1717
# Test binary, built with `go test -c`
1818
*.test
19+
*.db
1920

2021
# Output of the go coverage tool, specifically when used with LiteIDE
2122
*.out

Makefile

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ install:
77
curl -sL https://lib.chdb.io | bash
88

99
test:
10-
CGO_ENABLED=1 go test -v -coverprofile=coverage.out ./...
10+
go test -v -coverprofile=coverage.out ./...
1111

1212
run:
13-
CGO_ENABLED=1 go run main.go
13+
go run main.go
1414

1515
build:
16-
CGO_ENABLED=1 go build -ldflags '-extldflags "-Wl,-rpath,/usr/local/lib"' -o chdb-go main.go
16+
go build -o chdb-go main.go

chdb-purego/binding.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var (
3939
freeResult func(result *local_result)
4040
queryStableV2 func(argc int, argv []string) *local_result_v2
4141
freeResultV2 func(result *local_result_v2)
42-
connectChdb func(argc int, argv []string) **chdb_conn
42+
connectChdb func(argc int, argv []*byte) **chdb_conn
4343
closeConn func(conn **chdb_conn)
4444
queryConn func(conn *chdb_conn, query string, format string) *local_result_v2
4545
)

chdb-purego/chdb.go

+164-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ package chdbpurego
33
import (
44
"errors"
55
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
69
"unsafe"
10+
11+
"golang.org/x/sys/unix"
712
)
813

914
type result struct {
@@ -141,12 +146,76 @@ func (c *connection) Ready() bool {
141146
return false
142147
}
143148

149+
// NewConnection is the low level function to create a new connection to the chdb server.
150+
// using NewConnectionFromConnString is recommended.
151+
//
152+
// Deprecated: Use NewConnectionFromConnString instead. This function will be removed in a future version.
153+
//
144154
// Session will keep the state of query.
145155
// If path is None, it will create a temporary directory and use it as the database path
146156
// and the temporary directory will be removed when the session is closed.
147157
// You can also pass in a path to create a database at that path where will keep your data.
158+
// This is a thin wrapper around the connect_chdb C API.
159+
// the argc and argv should be like:
160+
// - argc = 1, argv = []string{"--path=/tmp/chdb"}
161+
// - argc = 2, argv = []string{"--path=/tmp/chdb", "--readonly=1"}
148162
//
149-
// You can also use a connection string to pass in the path and other parameters.
163+
// Important:
164+
// - There can be only one session at a time. If you want to create a new session, you need to close the existing one.
165+
// - Creating a new session will close the existing one.
166+
// - You need to ensure that the path exists before creating a new session. Or you can use NewConnectionFromConnString.
167+
func NewConnection(argc int, argv []string) (ChdbConn, error) {
168+
var new_argv []string
169+
if (argc > 0 && argv[0] != "clickhouse") || argc == 0 {
170+
new_argv = make([]string, argc+1)
171+
new_argv[0] = "clickhouse"
172+
copy(new_argv[1:], argv)
173+
} else {
174+
new_argv = argv
175+
}
176+
177+
// Remove ":memory:" if it is the only argument
178+
if len(new_argv) == 2 && (new_argv[1] == ":memory:" || new_argv[1] == "file::memory:") {
179+
new_argv = new_argv[:1]
180+
}
181+
182+
// Convert string slice to C-style char pointers in one step
183+
c_argv := make([]*byte, len(new_argv))
184+
for i, str := range new_argv {
185+
// Convert string to []byte and append null terminator
186+
bytes := append([]byte(str), 0)
187+
// Use &bytes[0] to get pointer to first byte
188+
c_argv[i] = &bytes[0]
189+
}
190+
191+
// debug print new_argv
192+
// for _, arg := range new_argv {
193+
// fmt.Println("arg: ", arg)
194+
// }
195+
196+
var conn **chdb_conn
197+
var err error
198+
func() {
199+
defer func() {
200+
if r := recover(); r != nil {
201+
err = fmt.Errorf("C++ exception: %v", r)
202+
}
203+
}()
204+
conn = connectChdb(len(new_argv), c_argv)
205+
}()
206+
207+
if err != nil {
208+
return nil, err
209+
}
210+
211+
if conn == nil {
212+
return nil, fmt.Errorf("could not create a chdb connection")
213+
}
214+
return newChdbConn(conn), nil
215+
}
216+
217+
// NewConnectionFromConnString creates a new connection to the chdb server using a connection string.
218+
// You can use a connection string to pass in the path and other parameters.
150219
// Examples:
151220
// - ":memory:" (for in-memory database)
152221
// - "test.db" (for relative path)
@@ -169,10 +238,99 @@ func (c *connection) Ready() bool {
169238
// Important:
170239
// - There can be only one session at a time. If you want to create a new session, you need to close the existing one.
171240
// - Creating a new session will close the existing one.
172-
func NewConnection(argc int, argv []string) (ChdbConn, error) {
173-
conn := connectChdb(argc, argv)
174-
if conn == nil {
175-
return nil, fmt.Errorf("could not create a chdb connection")
241+
func NewConnectionFromConnString(conn_string string) (ChdbConn, error) {
242+
if conn_string == "" || conn_string == ":memory:" {
243+
return NewConnection(0, []string{})
176244
}
177-
return newChdbConn(conn), nil
245+
246+
// Handle file: prefix
247+
workingStr := conn_string
248+
if strings.HasPrefix(workingStr, "file:") {
249+
workingStr = workingStr[5:]
250+
// Handle triple slash for absolute paths
251+
if strings.HasPrefix(workingStr, "///") {
252+
workingStr = workingStr[2:] // Remove two slashes, keep one
253+
}
254+
}
255+
256+
// Split path and parameters
257+
var path string
258+
var params []string
259+
if queryPos := strings.Index(workingStr, "?"); queryPos != -1 {
260+
path = workingStr[:queryPos]
261+
paramStr := workingStr[queryPos+1:]
262+
263+
// Parse parameters
264+
for _, param := range strings.Split(paramStr, "&") {
265+
if param == "" {
266+
continue
267+
}
268+
if eqPos := strings.Index(param, "="); eqPos != -1 {
269+
key := param[:eqPos]
270+
value := param[eqPos+1:]
271+
if key == "mode" && value == "ro" {
272+
params = append(params, "--readonly=1")
273+
} else if key == "udf_path" && value != "" {
274+
params = append(params, "--")
275+
params = append(params, "--user_scripts_path="+value)
276+
params = append(params, "--user_defined_executable_functions_config="+value+"/*.xml")
277+
} else {
278+
params = append(params, "--"+key+"="+value)
279+
}
280+
} else {
281+
params = append(params, "--"+param)
282+
}
283+
}
284+
} else {
285+
path = workingStr
286+
}
287+
288+
// Convert relative paths to absolute if needed
289+
if path != "" && !strings.HasPrefix(path, "/") && path != ":memory:" {
290+
absPath, err := filepath.Abs(path)
291+
if err != nil {
292+
return nil, fmt.Errorf("failed to resolve path: %s", path)
293+
}
294+
path = absPath
295+
}
296+
297+
// Check if path exists and handle directory creation/permissions
298+
if path != "" && path != ":memory:" {
299+
// Check if path exists
300+
_, err := os.Stat(path)
301+
if os.IsNotExist(err) {
302+
// Create directory if it doesn't exist
303+
if err := os.MkdirAll(path, 0755); err != nil {
304+
return nil, fmt.Errorf("failed to create directory: %s", path)
305+
}
306+
} else if err != nil {
307+
return nil, fmt.Errorf("failed to check directory: %s", path)
308+
}
309+
310+
// Check write permissions if not in readonly mode
311+
isReadOnly := false
312+
for _, param := range params {
313+
if param == "--readonly=1" {
314+
isReadOnly = true
315+
break
316+
}
317+
}
318+
319+
if !isReadOnly {
320+
// Check write permissions by attempting to create a file
321+
if err := unix.Access(path, unix.W_OK); err != nil {
322+
return nil, fmt.Errorf("no write permission for directory: %s", path)
323+
}
324+
}
325+
}
326+
327+
// Build arguments array
328+
argv := make([]string, 0, len(params)+2)
329+
argv = append(argv, "clickhouse")
330+
if path != "" && path != ":memory:" {
331+
argv = append(argv, "--path="+path)
332+
}
333+
argv = append(argv, params...)
334+
335+
return NewConnection(len(argv), argv)
178336
}

chdb-purego/chdb_test.go

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package chdbpurego
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestNewConnection(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
argc int
13+
argv []string
14+
wantErr bool
15+
}{
16+
{
17+
name: "empty args",
18+
argc: 0,
19+
argv: []string{},
20+
wantErr: false,
21+
},
22+
{
23+
name: "memory database",
24+
argc: 1,
25+
argv: []string{":memory:"},
26+
wantErr: false,
27+
},
28+
}
29+
30+
for _, tt := range tests {
31+
t.Run(tt.name, func(t *testing.T) {
32+
conn, err := NewConnection(tt.argc, tt.argv)
33+
if (err != nil) != tt.wantErr {
34+
t.Errorf("NewConnection() error = %v, wantErr %v", err, tt.wantErr)
35+
return
36+
}
37+
if conn == nil && !tt.wantErr {
38+
t.Error("NewConnection() returned nil connection without error")
39+
return
40+
}
41+
if conn != nil {
42+
defer conn.Close()
43+
if !conn.Ready() {
44+
t.Error("NewConnection() returned connection that is not ready")
45+
}
46+
}
47+
})
48+
}
49+
}
50+
51+
func TestNewConnectionFromConnString(t *testing.T) {
52+
// Create a temporary directory for testing
53+
tmpDir, err := os.MkdirTemp("", "chdb_test_*")
54+
if err != nil {
55+
t.Fatalf("Failed to create temp dir: %v", err)
56+
}
57+
defer os.RemoveAll(tmpDir)
58+
59+
tests := []struct {
60+
name string
61+
connStr string
62+
wantErr bool
63+
checkPath bool
64+
}{
65+
{
66+
name: "empty string",
67+
connStr: "",
68+
wantErr: false,
69+
},
70+
{
71+
name: "memory database",
72+
connStr: ":memory:",
73+
wantErr: false,
74+
},
75+
{
76+
name: "memory database with params",
77+
connStr: ":memory:?verbose&log-level=test",
78+
wantErr: false,
79+
},
80+
{
81+
name: "relative path",
82+
connStr: "test.db",
83+
wantErr: false,
84+
checkPath: true,
85+
},
86+
{
87+
name: "file prefix",
88+
connStr: "file:test.db",
89+
wantErr: false,
90+
checkPath: true,
91+
},
92+
{
93+
name: "absolute path",
94+
connStr: filepath.Join(tmpDir, "test.db"),
95+
wantErr: false,
96+
checkPath: true,
97+
},
98+
{
99+
name: "file prefix with absolute path",
100+
connStr: "file:" + filepath.Join(tmpDir, "test.db"),
101+
wantErr: false,
102+
checkPath: true,
103+
},
104+
// {
105+
// name: "readonly mode with existing dir",
106+
// connStr: filepath.Join(tmpDir, "readonly.db") + "?mode=ro",
107+
// wantErr: false,
108+
// checkPath: true,
109+
// },
110+
// {
111+
// name: "readonly mode with non-existing dir",
112+
// connStr: filepath.Join(tmpDir, "new_readonly.db") + "?mode=ro",
113+
// wantErr: true,
114+
// checkPath: true,
115+
// },
116+
{
117+
name: "write mode with existing dir",
118+
connStr: filepath.Join(tmpDir, "write.db"),
119+
wantErr: false,
120+
checkPath: true,
121+
},
122+
{
123+
name: "write mode with non-existing dir",
124+
connStr: filepath.Join(tmpDir, "new_write.db"),
125+
wantErr: false,
126+
checkPath: true,
127+
},
128+
}
129+
130+
for _, tt := range tests {
131+
t.Run(tt.name, func(t *testing.T) {
132+
conn, err := NewConnectionFromConnString(tt.connStr)
133+
if (err != nil) != tt.wantErr {
134+
t.Errorf("NewConnectionFromConnString() error = %v, wantErr %v", err, tt.wantErr)
135+
return
136+
}
137+
if conn == nil && !tt.wantErr {
138+
t.Error("NewConnectionFromConnString() returned nil connection without error")
139+
return
140+
}
141+
if conn != nil {
142+
defer conn.Close()
143+
if !conn.Ready() {
144+
t.Error("NewConnectionFromConnString() returned connection that is not ready")
145+
}
146+
147+
// Test a simple query to verify the connection works
148+
result, err := conn.Query("SELECT 1", "CSV")
149+
if err != nil {
150+
t.Errorf("Query failed: %v", err)
151+
return
152+
}
153+
if result == nil {
154+
t.Error("Query returned nil result")
155+
return
156+
}
157+
if result.Error() != nil {
158+
t.Errorf("Query result has error: %v", result.Error())
159+
return
160+
}
161+
if result.String() != "1\n" {
162+
t.Errorf("Query result = %v, want %v", result.String(), "1\n")
163+
}
164+
}
165+
})
166+
}
167+
}

0 commit comments

Comments
 (0)