@@ -3,7 +3,12 @@ package chdbpurego
33import (
44 "errors"
55 "fmt"
6+ "os"
7+ "path/filepath"
8+ "strings"
69 "unsafe"
10+
11+ "golang.org/x/sys/unix"
712)
813
914type 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}
0 commit comments