chdb-ruby 0.1.0.rc.2 → 0.2.0.rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77408619eae7f84800fa14da51844d0180ea5d4cb7513d2f2c7f2c273f287787
4
- data.tar.gz: 5cff1f1590fa5a70241d1a2e1ee1068e965487c6741ad7523921b5be0a35a8e8
3
+ metadata.gz: d3dffafce42be0cf41b18bdc22e47ae17a4dfea85169a641b2068a9d0e54b306
4
+ data.tar.gz: e0cde6afb8872dd2a72f5dc8b90392ec0163707daa430907aa126e63a57bbf77
5
5
  SHA512:
6
- metadata.gz: 1a501dd7c418c3af1edd49e77e69a50b6229cd8610121d7200d5e04633c61eeafe4fc87615f24c4e6d68a99bda3daa912575a6ecf919960e1398bef253d02855
7
- data.tar.gz: 07752df52ce67356c9f32b37d6444bdcdb5d49c274d61d67e26c7752694aedcd53184a682bea3408057d217152a5584e4c71c6230559b70bf289162bcaa7744e
6
+ metadata.gz: d198f71a5348388d2b49eb5819014490d0300a20fb1d48c8342da35162d41b650d3c872f1d65b15777d2a73a47b9ca5438f551a066c05b59faab2f2b6148bd02
7
+ data.tar.gz: f73e40324a300d87849350637257a593f484ba2b8a8f7e40c671969ed9dbc2c1737437641dbe96a7a119d22b33bff753a514bd66f612453c0f9305ffea620a4a
data/README.md CHANGED
@@ -20,69 +20,122 @@ Note that this module is only compatible with ChDB 3.0.0 or newer.
20
20
 
21
21
  ## Quick start
22
22
 
23
- ``` ruby
23
+ Before using chdb-ruby, install the gem first. This will download the libchdb C++ library dependencies, so please be patient:
24
+ ```bash
25
+ gem install chdb-ruby
26
+ ```
27
+
28
+ Below are examples of common interfaces usage:
29
+
30
+ ```ruby
24
31
  require 'chdb'
25
32
 
26
33
  # Open a database
27
- db = ChDB::Database.new('test_db', results_as_hash: true)
34
+ # Parameter explanation:
35
+ # 1. path supports two formats:
36
+ # - ":memory:" in-memory temporary database (data destroyed on close)
37
+ # - "file:/path/to/db" file-based persistent database
38
+ # Configuration parameters can be appended via URL-style query (e.g. 'file:test.db?results_as_hash=true')
39
+ # 2. options hash supports:
40
+ # - results_as_hash: controls whether result sets return as hashes (default: arrays)
41
+ db = ChDB::Database.new('file:test.db', results_as_hash: true)
42
+
43
+ # Create a database
44
+ db.execute('CREATE DATABASE IF NOT EXISTS test')
28
45
 
29
46
  # Create a table
47
+ db.execute('DROP TABLE IF EXISTS test.test_table')
30
48
  rows = db.execute <<-SQL
31
- CREATE TABLE test_table(
49
+ CREATE TABLE test.test_table(
32
50
  id Int32,
33
51
  name String)
34
52
  ENGINE = MergeTree()
35
- ORDER BY id);
53
+ ORDER BY id
36
54
  SQL
37
55
 
38
56
  # Execute a few inserts
39
57
  {
40
- 1 => 'Alice',
41
- 2 => 'Bob'
58
+ 1 => 'Alice',
59
+ 2 => 'Bob'
42
60
  }.each do |pair|
43
- db.execute 'INSERT INTO test_table VALUES ( ?, ? )', pair
61
+ db.execute 'INSERT INTO test.test_table VALUES ( ?, ? )', pair
44
62
  end
45
63
 
46
64
  # Find a few rows
47
- db.execute('SELECT * FROM test_table ORDER BY id') do |row|
65
+ db.execute('SELECT * FROM test.test_table ORDER BY id') do |row|
48
66
  p row
49
67
  end
50
68
  # [{ 'id' => '1', 'name' => 'Alice' },
51
69
  # { 'id' => '2', 'name' => 'Bob' }]
52
70
 
71
+ # When you need to open another database, you must first close the previous database
72
+ db.close()
73
+
53
74
  # Open another database
54
- db = ChDB::Database.new 'test2.db'
75
+ db = ChDB::Database.new 'file:test.db'
55
76
 
56
77
  # Create another table
78
+ db.execute('DROP TABLE IF EXISTS test.test2_table')
57
79
  rows = db.execute <<-SQL
58
- CREATE TABLE test2_table(
80
+ CREATE TABLE test.test2_table(
59
81
  id Int32,
60
82
  name String)
61
83
  ENGINE = MergeTree()
62
- ORDER BY id");
84
+ ORDER BY id
63
85
  SQL
64
86
 
65
87
  # Execute inserts with parameter markers
66
- db.execute('INSERT INTO test2_table (id, name)
88
+ db.execute('INSERT INTO test.test2_table (id, name)
67
89
  VALUES (?, ?)', [3, 'Charlie'])
68
90
 
69
- db.execute2('SELECT * FROM test2_table') do |row|
91
+ # Find rows with the first row displaying column names
92
+ db.execute2('SELECT * FROM test.test2_table') do |row|
70
93
  p row
71
94
  end
72
- # [['id', 'name'], [3, 'Charlie']],
95
+ # ["id", "name"]
96
+ # ["3", "Charlie"]
97
+
98
+ # Close the database
99
+ db.close()
100
+
101
+ # Use ChDB::Database.open to automatically close the database connection:
102
+ ChDB::Database.open('file:test.db') do |db|
103
+ result = db.execute('SELECT 1')
104
+ p result.to_a # => [["1"]]
105
+ end
106
+
107
+ # Query with specific output formats (CSV, JSON, etc.):
108
+ # See more details at https://clickhouse.com/docs/interfaces/formats.
109
+ ChDB::Database.open(':memory:') do |db|
110
+ csv_data = db.query_with_format('SELECT 1 as a, 2 as b', 'CSV')
111
+ p csv_data
112
+ # "1,2\n"
113
+
114
+ json_data = db.query_with_format('SELECT 1 as a, 2 as b', 'JSON')
115
+ p json_data
116
+ end
117
+
118
+ # Execute streaming query
119
+ ChDB::Database.open(':memory:') do |db|
120
+ total_rows = 0
121
+ collected = []
122
+ db.send_query('SELECT * FROM numbers(200000)') do |chunk|
123
+ collected << chunk.buf
124
+ total_rows += chunk.rows_read
125
+ end
126
+ p total_rows # => 200000
127
+ end
73
128
  ```
74
129
 
75
130
  ## Thread Safety
76
131
 
77
- When using `ChDB::Database.new` to open a session, all read/write operations within that session are thread-safe. However, currently only one active session is allowed per process. Therefore, when you need to open another session, you must first close the previous session.
78
-
79
- For example, the following code is fine because only the database
80
- instance is shared among threads:
132
+ When using `ChDB::Database.new` or `ChDB::Database.open` to open a database connection, all read/write operations within that session are thread-safe. However, currently only one active database connection is allowed per process. Therefore, when you need to open another database connection, you must first close the previous connection.
133
+ **Please note that `ChDB::Database.new`, `ChDB::Database.open`, and `ChDB::Database.close` methods themselves are not thread-safe.** If used in multi-threaded environments, external synchronization must be implemented to prevent concurrent calls to these methods, which could lead to undefined behavior.
81
134
 
82
135
  ```ruby
83
136
  require 'chdb'
84
137
 
85
- db = ChDB::Database.new ":memory:'
138
+ db = ChDB::Database.new ':memory:'
86
139
 
87
140
  latch = Queue.new
88
141
 
@@ -95,6 +148,9 @@ ts = 10.times.map {
95
148
  10.times { latch << nil }
96
149
 
97
150
  p ts.map(&:value)
151
+ # [[["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]]]
152
+
153
+ db.close()
98
154
  ```
99
155
 
100
156
  Other instances can be shared among threads, but they require that you provide
data/dependencies.yml CHANGED
@@ -1,2 +1,2 @@
1
1
  chdb:
2
- version: "3.1.2"
2
+ version: "3.2.0"
data/ext/chdb/chdb.c CHANGED
@@ -6,17 +6,7 @@
6
6
  #include "connection.h"
7
7
  #include "exception.h"
8
8
  #include "local_result.h"
9
-
10
- void init_chdb_constants()
11
- {
12
- VALUE mChDB = rb_define_module("ChDB");
13
- VALUE mChDBConstants = rb_define_module_under(mChDB, "Constants");
14
- VALUE mmChDBOpen = rb_define_module_under(mChDBConstants, "Open");
15
-
16
- rb_define_const(mmChDBOpen, "READONLY", INT2FIX(CHDB_OPEN_READONLY));
17
- rb_define_const(mmChDBOpen, "READWRITE", INT2FIX(CHDB_OPEN_READWRITE));
18
- rb_define_const(mmChDBOpen, "CREATE", INT2FIX(CHDB_OPEN_CREATE));
19
- }
9
+ #include "streaming_result.h"
20
10
 
21
11
  void Init_chdb_native()
22
12
  {
@@ -26,6 +16,7 @@ void Init_chdb_native()
26
16
  init_chdb_handle();
27
17
  init_chdb_constants();
28
18
  init_local_result();
19
+ init_streaming_result();
29
20
  init_connection();
30
21
 
31
22
  DEBUG_PRINT("chdb extension initialized successfully");
@@ -2,30 +2,53 @@
2
2
 
3
3
  #include <dlfcn.h>
4
4
  #include <ruby.h>
5
-
5
+ #include "constants.h"
6
6
  #include "exception.h"
7
7
 
8
8
  void *chdb_handle = NULL;
9
9
  connect_chdb_func connect_chdb_ptr = NULL;
10
10
  close_conn_func close_conn_ptr = NULL;
11
11
  query_conn_func query_conn_ptr = NULL;
12
+ free_result_v2_func free_result_v2_ptr = NULL;
13
+ query_conn_streaming_func query_conn_streaming_ptr = NULL;
14
+ chdb_streaming_result_error_func chdb_streaming_result_error_ptr = NULL;
15
+ chdb_streaming_fetch_result_func chdb_streaming_fetch_result_ptr = NULL;
16
+ chdb_streaming_cancel_query_func chdb_streaming_cancel_query_ptr = NULL;
17
+ chdb_destroy_result_func chdb_destroy_result_ptr = NULL;
12
18
 
13
- VALUE get_chdb_rb_path(void)
19
+ VALUE get_chdb_rb_path()
14
20
  {
15
21
  VALUE chdb_module = rb_const_get(rb_cObject, rb_intern("ChDB"));
16
22
  return rb_funcall(chdb_module, rb_intern("lib_file_path"), 0);
17
23
  }
18
24
 
25
+ void close_chdb_handle()
26
+ {
27
+ if (chdb_handle)
28
+ {
29
+ dlclose(chdb_handle);
30
+ chdb_handle = NULL;
31
+ DEBUG_PRINT("Close chdb handle");
32
+ }
33
+ }
34
+
19
35
  void init_chdb_handle()
20
36
  {
21
37
  VALUE rb_path = get_chdb_rb_path();
22
38
  VALUE lib_dir = rb_file_dirname(rb_file_dirname(rb_path));
23
39
  VALUE lib_path = rb_str_cat2(lib_dir, "/lib/chdb/lib/libchdb.so");
24
- // printf("chdb.rb path from Ruby: %s\n", StringValueCStr(lib_path));
40
+
41
+ DEBUG_PRINT("chdb.rb path from Ruby: %s\n", StringValueCStr(lib_path));
25
42
 
26
43
  connect_chdb_ptr = NULL;
27
44
  close_conn_ptr = NULL;
28
45
  query_conn_ptr = NULL;
46
+ free_result_v2_ptr = NULL;
47
+ query_conn_streaming_ptr = NULL;
48
+ chdb_streaming_result_error_ptr = NULL;
49
+ chdb_streaming_fetch_result_ptr = NULL;
50
+ chdb_streaming_cancel_query_ptr = NULL;
51
+ chdb_destroy_result_ptr = NULL;
29
52
 
30
53
  chdb_handle = dlopen(RSTRING_PTR(lib_path), RTLD_LAZY | RTLD_GLOBAL);
31
54
  if (!chdb_handle)
@@ -37,13 +60,32 @@ void init_chdb_handle()
37
60
  connect_chdb_ptr = (connect_chdb_func)dlsym(chdb_handle, "connect_chdb");
38
61
  close_conn_ptr = (close_conn_func)dlsym(chdb_handle, "close_conn");
39
62
  query_conn_ptr = (query_conn_func)dlsym(chdb_handle, "query_conn");
63
+ free_result_v2_ptr = (free_result_v2_func)dlsym(chdb_handle, "free_result_v2");
64
+ query_conn_streaming_ptr = (query_conn_streaming_func)dlsym(chdb_handle, "query_conn_streaming");
65
+ chdb_streaming_result_error_ptr = (chdb_streaming_result_error_func)dlsym(chdb_handle, "chdb_streaming_result_error");
66
+ chdb_streaming_fetch_result_ptr = (chdb_streaming_fetch_result_func)dlsym(chdb_handle, "chdb_streaming_fetch_result");
67
+ chdb_streaming_cancel_query_ptr = (chdb_streaming_cancel_query_func)dlsym(chdb_handle, "chdb_streaming_cancel_query");
68
+ chdb_destroy_result_ptr = (chdb_destroy_result_func)dlsym(chdb_handle, "chdb_destroy_result");
40
69
 
41
- if (!connect_chdb_ptr || !close_conn_ptr || !query_conn_ptr)
70
+ if (!connect_chdb_ptr || !close_conn_ptr || !query_conn_ptr || !free_result_v2_ptr ||
71
+ !query_conn_streaming_ptr || !chdb_streaming_result_error_ptr || !chdb_streaming_fetch_result_ptr ||
72
+ !chdb_streaming_cancel_query_ptr || !chdb_destroy_result_ptr)
42
73
  {
43
- rb_raise(cChDBError, "Symbol loading failed: %s\nMissing functions: connect_chdb(%p) close_conn(%p) query_conn(%p)",
74
+ close_chdb_handle();
75
+
76
+ rb_raise(cChDBError,
77
+ "Symbol loading failed: %s\nMissing functions: connect_chdb(%p), close_conn(%p), query_conn(%p), free_result_v2(%p), query_conn_streaming(%p), chdb_streaming_result_error(%p), chdb_streaming_fetch_result(%p), chdb_streaming_cancel_query(%p), chdb_destroy_result(%p)",
44
78
  dlerror(),
45
79
  (void*)connect_chdb_ptr,
46
80
  (void*)close_conn_ptr,
47
- (void*)query_conn_ptr);
81
+ (void*)query_conn_ptr,
82
+ (void*)free_result_v2_ptr,
83
+ (void*)query_conn_streaming_ptr,
84
+ (void*)chdb_streaming_result_error_ptr,
85
+ (void*)chdb_streaming_fetch_result_ptr,
86
+ (void*)chdb_streaming_cancel_query_ptr,
87
+ (void*)chdb_destroy_result_ptr);
48
88
  }
49
- }
89
+
90
+ rb_set_end_proc(close_chdb_handle, 0);
91
+ }
@@ -1,16 +1,30 @@
1
1
  #ifndef CHDB_HANDLE_H
2
2
  #define CHDB_HANDLE_H
3
3
 
4
+ #include "include/chdb.h"
5
+
4
6
  typedef struct chdb_conn **(*connect_chdb_func)(int, char**);
5
7
  typedef void (*close_conn_func)(struct chdb_conn**);
6
8
  typedef struct local_result_v2 *(*query_conn_func)(struct chdb_conn*, const char*, const char*);
9
+ typedef void (*free_result_v2_func)(struct local_result_v2*);
10
+ typedef chdb_streaming_result *(*query_conn_streaming_func)(struct chdb_conn*, const char*, const char*);
11
+ typedef const char *(*chdb_streaming_result_error_func)(chdb_streaming_result*);
12
+ typedef struct local_result_v2 *(*chdb_streaming_fetch_result_func)(struct chdb_conn*, chdb_streaming_result*);
13
+ typedef void (*chdb_streaming_cancel_query_func)(struct chdb_conn*, chdb_streaming_result*);
14
+ typedef void (*chdb_destroy_result_func)(chdb_streaming_result*);
7
15
 
8
16
  extern connect_chdb_func connect_chdb_ptr;
9
17
  extern close_conn_func close_conn_ptr;
10
18
  extern query_conn_func query_conn_ptr;
19
+ extern free_result_v2_func free_result_v2_ptr;
20
+ extern query_conn_streaming_func query_conn_streaming_ptr;
21
+ extern chdb_streaming_result_error_func chdb_streaming_result_error_ptr;
22
+ extern chdb_streaming_fetch_result_func chdb_streaming_fetch_result_ptr;
23
+ extern chdb_streaming_cancel_query_func chdb_streaming_cancel_query_ptr;
24
+ extern chdb_destroy_result_func chdb_destroy_result_ptr;
11
25
 
12
26
  extern void *chdb_handle;
13
27
 
14
28
  void init_chdb_handle();
15
29
 
16
- #endif
30
+ #endif
@@ -5,11 +5,13 @@
5
5
  #include "exception.h"
6
6
  #include "include/chdb.h"
7
7
  #include "local_result.h"
8
+ #include "streaming_result.h"
8
9
 
9
- static void connection_free(void *ptr)
10
+ void connection_free(void *ptr)
10
11
  {
11
12
  Connection *conn = (Connection *)ptr;
12
- DEBUG_PRINT("Closing connection: %p", (void*)conn->c_conn);
13
+ DEBUG_PRINT("Closing connection in connection_free: %p", (void*)conn->c_conn);
14
+
13
15
  if (conn->c_conn)
14
16
  {
15
17
  close_conn_ptr(conn->c_conn);
@@ -31,6 +33,9 @@ void init_connection()
31
33
  rb_define_alloc_func(cConnection, connection_alloc);
32
34
  rb_define_method(cConnection, "initialize", connection_initialize, 2);
33
35
  rb_define_method(cConnection, "query", connection_query, 2);
36
+ rb_define_method(cConnection, "send_query", connection_streaming_query, 2);
37
+ rb_define_method(cConnection, "fetch_streaming_result", connection_streaming_fecth_result, 1);
38
+ rb_define_method(cConnection, "cancel_streaming_query", connection_streaming_cancel_query, 1);
34
39
  rb_define_method(cConnection, "close", connection_close, 0);
35
40
  }
36
41
 
@@ -67,7 +72,6 @@ VALUE connection_initialize(VALUE self, VALUE argc, VALUE argv)
67
72
  }
68
73
 
69
74
  xfree(c_argv);
70
- rb_gc_unregister_address(&argv);
71
75
  return self;
72
76
  }
73
77
 
@@ -93,6 +97,7 @@ VALUE connection_query(VALUE self, VALUE query, VALUE format)
93
97
  if (c_result->error_message)
94
98
  {
95
99
  VALUE error_message = rb_str_new_cstr(c_result->error_message);
100
+ free_result_v2_ptr(c_result);
96
101
  rb_raise(cChDBError, "CHDB error: %s", StringValueCStr(error_message));
97
102
  }
98
103
 
@@ -104,12 +109,90 @@ VALUE connection_query(VALUE self, VALUE query, VALUE format)
104
109
  return result_obj;
105
110
  }
106
111
 
112
+ VALUE connection_streaming_query(VALUE self, VALUE query, VALUE format)
113
+ {
114
+ Connection *conn;
115
+ TypedData_Get_Struct(self, Connection, &ConnectionType, conn);
116
+
117
+ Check_Type(query, T_STRING);
118
+ Check_Type(format, T_STRING);
119
+
120
+ chdb_streaming_result *c_result = query_conn_streaming_ptr(
121
+ *conn->c_conn,
122
+ StringValueCStr(query),
123
+ StringValueCStr(format)
124
+ );
125
+
126
+ if (!c_result)
127
+ {
128
+ rb_raise(cChDBError, "Query failed with nil streaming result");
129
+ }
130
+
131
+ const char *error = chdb_streaming_result_error_ptr(c_result);
132
+ if (error)
133
+ {
134
+ VALUE error_message = rb_str_new_cstr(error);
135
+ chdb_destroy_result_ptr(c_result);
136
+ rb_raise(cChDBError, "CHDB error: %s", StringValueCStr(error_message));
137
+ }
138
+
139
+ VALUE result_obj = rb_class_new_instance(0, NULL, cStreamingResult);
140
+ StreamingResult *result;
141
+ TypedData_Get_Struct(result_obj, StreamingResult, &StreamingResultType, result);
142
+ result->c_result = c_result;
143
+
144
+ return result_obj;
145
+ }
146
+
147
+ VALUE connection_streaming_fecth_result(VALUE self, VALUE streaming_result)
148
+ {
149
+ Connection *conn;
150
+ TypedData_Get_Struct(self, Connection, &ConnectionType, conn);
151
+
152
+ StreamingResult *result;
153
+ TypedData_Get_Struct(streaming_result, StreamingResult, &StreamingResultType, result);
154
+
155
+ struct local_result_v2 *c_result = chdb_streaming_fetch_result_ptr(*conn->c_conn, result->c_result);
156
+
157
+ if (!c_result)
158
+ {
159
+ rb_raise(cChDBError, "Failed to fetch streaming result");
160
+ }
161
+
162
+ if (c_result->error_message)
163
+ {
164
+ VALUE error_message = rb_str_new_cstr(c_result->error_message);
165
+ free_result_v2_ptr(c_result);
166
+ rb_raise(cChDBError, "CHDB error: %s", StringValueCStr(error_message));
167
+ }
168
+
169
+ VALUE result_obj = rb_class_new_instance(0, NULL, cLocalResult);
170
+ LocalResult *local_result;
171
+ TypedData_Get_Struct(result_obj, LocalResult, &LocalResultType, local_result);
172
+ local_result->c_result = c_result;
173
+
174
+ return result_obj;
175
+ }
176
+
177
+ VALUE connection_streaming_cancel_query(VALUE self, VALUE streaming_result)
178
+ {
179
+ Connection *conn;
180
+ TypedData_Get_Struct(self, Connection, &ConnectionType, conn);
181
+
182
+ StreamingResult *result;
183
+ TypedData_Get_Struct(streaming_result, StreamingResult, &StreamingResultType, result);
184
+
185
+ chdb_streaming_cancel_query_ptr(*conn->c_conn, result->c_result);
186
+ return Qnil;
187
+ }
188
+
107
189
  VALUE connection_close(VALUE self)
108
190
  {
109
191
  Connection *conn;
110
192
  TypedData_Get_Struct(self, Connection, &ConnectionType, conn);
193
+ DEBUG_PRINT("Closing connection in connection_close: %p", (void*)conn->c_conn);
111
194
 
112
- if (conn->c_conn)
195
+ if (conn && conn->c_conn)
113
196
  {
114
197
  close_conn_ptr(conn->c_conn);
115
198
  conn->c_conn = NULL;
@@ -18,6 +18,12 @@ VALUE connection_initialize(VALUE self, VALUE argc, VALUE argv);
18
18
 
19
19
  VALUE connection_query(VALUE self, VALUE query, VALUE format);
20
20
 
21
+ VALUE connection_streaming_query(VALUE self, VALUE query, VALUE format);
22
+
23
+ VALUE connection_streaming_fecth_result(VALUE self, VALUE streaming_result);
24
+
25
+ VALUE connection_streaming_cancel_query(VALUE self, VALUE streaming_result);
26
+
21
27
  VALUE connection_close(VALUE self);
22
28
 
23
29
  #endif
@@ -0,0 +1,14 @@
1
+ #include "constants.h"
2
+
3
+ #include <ruby.h>
4
+
5
+ void init_chdb_constants()
6
+ {
7
+ VALUE mChDB = rb_define_module("ChDB");
8
+ VALUE mChDBConstants = rb_define_module_under(mChDB, "Constants");
9
+ VALUE mmChDBOpen = rb_define_module_under(mChDBConstants, "Open");
10
+
11
+ rb_define_const(mmChDBOpen, "READONLY", INT2FIX(CHDB_OPEN_READONLY));
12
+ rb_define_const(mmChDBOpen, "READWRITE", INT2FIX(CHDB_OPEN_READWRITE));
13
+ rb_define_const(mmChDBOpen, "CREATE", INT2FIX(CHDB_OPEN_CREATE));
14
+ }
data/ext/chdb/constants.h CHANGED
@@ -12,4 +12,6 @@
12
12
  #define DEBUG_PRINT(fmt, ...) ((void)0)
13
13
  #endif
14
14
 
15
+ void init_chdb_constants();
16
+
15
17
  #endif
data/ext/chdb/exception.c CHANGED
@@ -12,5 +12,6 @@ void init_exception()
12
12
  else
13
13
  {
14
14
  cChDBError = rb_define_class_under(mChDB, "Exception", rb_eStandardError);
15
+ rb_global_variable(&cChDBError);
15
16
  }
16
17
  }
@@ -1,17 +1,18 @@
1
1
  #include "local_result.h"
2
2
 
3
3
  #include "constants.h"
4
- #include "include/chdb.h"
4
+ #include "chdb_handle.h"
5
5
 
6
6
  VALUE cLocalResult;
7
7
 
8
- static void local_result_free(void *ptr)
8
+ void local_result_free(void *ptr)
9
9
  {
10
10
  LocalResult *result = (LocalResult *)ptr;
11
11
  DEBUG_PRINT("Freeing LocalResult: %p", (void*)result);
12
12
  if (result->c_result)
13
13
  {
14
- free_result_v2(result->c_result);
14
+ DEBUG_PRINT("Freeing local_result_v2: %p", (void*)result->c_result);
15
+ free_result_v2_ptr(result->c_result);
15
16
  }
16
17
  free(result);
17
18
  }
@@ -3,6 +3,8 @@
3
3
 
4
4
  #include <ruby.h>
5
5
 
6
+ #include "include/chdb.h"
7
+
6
8
  typedef struct
7
9
  {
8
10
  struct local_result_v2 *c_result;
@@ -0,0 +1,39 @@
1
+ #include "streaming_result.h"
2
+
3
+ #include "constants.h"
4
+ #include "chdb_handle.h"
5
+
6
+ VALUE cStreamingResult;
7
+
8
+ void streaming_result_free(void *ptr)
9
+ {
10
+ StreamingResult *result = (StreamingResult *)ptr;
11
+ DEBUG_PRINT("Freeing StreamingResult: %p", (void*)result);
12
+ if (result->c_result)
13
+ {
14
+ DEBUG_PRINT("Freeing chdb_streaming_result: %p", (void*)result->c_result);
15
+ chdb_destroy_result_ptr(result->c_result);
16
+ }
17
+ free(result);
18
+ }
19
+
20
+ const rb_data_type_t StreamingResultType =
21
+ {
22
+ "StreamingResult",
23
+ {NULL, streaming_result_free, NULL},
24
+ };
25
+
26
+ void init_streaming_result()
27
+ {
28
+ VALUE mChDB = rb_define_module("ChDB");
29
+ cStreamingResult = rb_define_class_under(mChDB, "StreamingResult", rb_cObject);
30
+ rb_define_alloc_func(cStreamingResult, streaming_result_alloc);
31
+ }
32
+
33
+ VALUE streaming_result_alloc(VALUE klass)
34
+ {
35
+ StreamingResult *result = ALLOC(StreamingResult);
36
+ DEBUG_PRINT("Allocating StreamingResult: %p", (void*)result);
37
+ result->c_result = NULL;
38
+ return rb_data_typed_object_wrap(klass, result, &StreamingResultType);
39
+ }
@@ -0,0 +1,20 @@
1
+ #ifndef CHDB_STREAMING_RESULT_H
2
+ #define CHDB_STREAMING_RESULT_H
3
+
4
+ #include <ruby.h>
5
+
6
+ #include "include/chdb.h"
7
+
8
+ typedef struct
9
+ {
10
+ chdb_streaming_result *c_result;
11
+ } StreamingResult;
12
+
13
+ extern VALUE cStreamingResult;
14
+ extern const rb_data_type_t StreamingResultType;
15
+
16
+ void init_streaming_result();
17
+
18
+ VALUE streaming_result_alloc(VALUE klass);
19
+
20
+ #endif
@@ -23,7 +23,7 @@ module ChDB
23
23
 
24
24
  def generate_arguments # rubocop:disable Metrics/MethodLength
25
25
  args = ['clickhouse', "--path=#{@dir_path}"]
26
- excluded_keys = %i[results_as_hash readonly readwrite flags]
26
+ excluded_keys = %w[results_as_hash readonly readwrite flags]
27
27
 
28
28
  @query_params.each do |key, value|
29
29
  next if excluded_keys.include?(key)
@@ -72,8 +72,7 @@ module ChDB
72
72
  end
73
73
 
74
74
  def merge_options(options)
75
- @query_params = @query_params.merge(options)
76
- # @query_params = @query_params.merge(options.transform_keys(&:to_s))
75
+ @query_params = @query_params.merge(options.transform_keys(&:to_s))
77
76
  end
78
77
 
79
78
  def directory_path(path)
@@ -87,31 +86,29 @@ module ChDB
87
86
  end
88
87
 
89
88
  def ensure_directory_exists
90
- if @mode.nobits?(Constants::Open::CREATE)
91
- raise DirectoryNotFoundException, "Directory #{@dir_path} required" unless Dir.exist?(@dir_path)
89
+ return if Dir.exist?(@dir_path)
92
90
 
93
- return
94
- end
91
+ raise DirectoryNotFoundException, "Directory #{@dir_path} required" if @mode.nobits?(Constants::Open::CREATE)
95
92
 
96
93
  FileUtils.mkdir_p(@dir_path, mode: 0o755)
97
94
  end
98
95
 
99
96
  def check_params # rubocop:disable Metrics/MethodLength
100
97
  @mode = Constants::Open::READWRITE | Constants::Open::CREATE
101
- @mode = Constants::Open::READONLY if @query_params[:readonly]
98
+ @mode = Constants::Open::READONLY if @query_params['readonly']
102
99
 
103
- if @query_params[:readwrite]
104
- raise InvalidArgumentException, 'conflicting options: readonly and readwrite' if @query_params[:readonly]
100
+ if @query_params['readwrite']
101
+ raise InvalidArgumentException, 'conflicting options: readonly and readwrite' if @query_params['readonly']
105
102
 
106
103
  @mode = Constants::Open::READWRITE
107
104
  end
108
105
 
109
- return unless @query_params[:flags]
110
- if @query_params[:readonly] || @query_params[:readwrite]
106
+ return unless @query_params['flags']
107
+ if @query_params['readonly'] || @query_params['readwrite']
111
108
  raise InvalidArgumentException, 'conflicting options: flags with readonly and/or readwrite'
112
109
  end
113
110
 
114
- @mode = @query_params[:flags]
111
+ @mode = @query_params['flags']
115
112
  end
116
113
 
117
114
  def remove_file_prefix(str)
data/lib/chdb/database.rb CHANGED
@@ -13,6 +13,8 @@ module ChDB
13
13
  # Represents a database connection and provides methods to interact with the database.
14
14
  class Database # rubocop:disable Metrics/ClassLength
15
15
  class << self
16
+ @@instance = nil # rubocop:disable Style/ClassVars
17
+
16
18
  # Without block works exactly as new.
17
19
  # With block, like new closes the database at the end, but unlike new
18
20
  # returns the result of the block instead of the database instance.
@@ -36,16 +38,20 @@ module ChDB
36
38
  attr_accessor :results_as_hash, :conn
37
39
 
38
40
  def initialize(file, options = {}) # rubocop:disable Metrics/MethodLength
41
+ raise InternalException, 'Existing database instance is not closed' if @@instance
42
+
39
43
  file = file.to_path if file.respond_to? :to_path
40
44
 
41
45
  @data_path = DataPath.new(file, options)
42
- @results_as_hash = @data_path.query_params[:results_as_hash]
46
+ @results_as_hash = @data_path.query_params['results_as_hash'] || false
43
47
  @readonly = @data_path.mode & Constants::Open::READONLY != 0
44
48
 
45
49
  argv = @data_path.generate_arguments
46
50
  @conn = ChDB::Connection.new(argv.size, argv)
47
51
  @closed = false
48
52
 
53
+ @@instance = self # rubocop:disable Style/ClassVars
54
+
49
55
  return unless block_given?
50
56
 
51
57
  begin
@@ -61,6 +67,8 @@ module ChDB
61
67
  @data_path.close if @data_path.respond_to?(:close)
62
68
  @conn.close if @conn.respond_to?(:close)
63
69
  @closed = true
70
+
71
+ @@instance = nil # rubocop:disable Style/ClassVars
64
72
  end
65
73
 
66
74
  def closed?
@@ -86,14 +94,14 @@ module ChDB
86
94
  end
87
95
  end
88
96
 
89
- def execute2(sql, *bind_vars, &) # rubocop:disable Metrics/MethodLength
97
+ def execute2(sql, *bind_vars, &block) # rubocop:disable Metrics/MethodLength
90
98
  prepare(sql) do |stmt|
91
99
  result = stmt.execute(*bind_vars)
92
100
  stmt.parse
93
101
 
94
- if block_given?
102
+ if block
95
103
  yield stmt.columns
96
- result.each(&)
104
+ result.each(&block)
97
105
  else
98
106
  return result.each_with_object([stmt.columns]) do |row, arr|
99
107
  arr << row
@@ -111,7 +119,7 @@ module ChDB
111
119
  end
112
120
  end
113
121
 
114
- def query_with_format(sql, bind_vars = [], format = 'CSV')
122
+ def query_with_format(sql, format = 'CSV', bind_vars = [])
115
123
  result = prepare(sql).execute_with_format(bind_vars, format)
116
124
  if block_given?
117
125
  yield result
@@ -120,6 +128,18 @@ module ChDB
120
128
  end
121
129
  end
122
130
 
131
+ def send_query(sql, format = 'CSV', bind_vars = [], &block)
132
+ prepare(sql) do |stmt|
133
+ result = stmt.send_query(bind_vars, format)
134
+
135
+ if block
136
+ result.each(&block)
137
+ else
138
+ result
139
+ end
140
+ end
141
+ end
142
+
123
143
  def get_first_row(sql, *bind_vars)
124
144
  execute(sql, *bind_vars).first
125
145
  end
@@ -11,14 +11,8 @@ module ChDB
11
11
  def bind_params(*bind_vars)
12
12
  index = 1
13
13
  bind_vars.flatten.each do |var|
14
- if var.is_a?(Hash)
15
- # TODO: Hash-style parameter binding not yet implemented
16
- # Currently using positional parameters instead of named parameters
17
- var.each { |key, val| bind_param key, val }
18
- else
19
- bind_param index, var
20
- index += 1
21
- end
14
+ bind_param index, var
15
+ index += 1
22
16
  end
23
17
  end
24
18
  end
@@ -54,4 +54,44 @@ module ChDB
54
54
  class HashResultSet < ResultSet # :nodoc:
55
55
  alias next next_hash
56
56
  end
57
+
58
+ class StreamingResultSet
59
+ include Enumerable
60
+
61
+ def initialize(db, streaming_result)
62
+ @db = db
63
+ @streaming_result = streaming_result
64
+ @done = false
65
+ end
66
+
67
+ def eof?
68
+ @done
69
+ end
70
+
71
+ def next
72
+ return nil if @done
73
+
74
+ result = @db.conn.fetch_streaming_result(@streaming_result)
75
+
76
+ if result.nil? || result.rows_read.zero?
77
+ @done = true
78
+ nil
79
+ else
80
+ result
81
+ end
82
+ end
83
+
84
+ def each
85
+ while (node = self.next)
86
+ yield node
87
+ end
88
+ end
89
+
90
+ def cancel
91
+ return nil if @done
92
+
93
+ @db.conn.cancel_streaming_query(@streaming_result)
94
+ @done = true
95
+ end
96
+ end
57
97
  end
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'chdb/errors'
4
+
3
5
  module ChDB
4
6
  # This module provides functionality for processing SQL queries,
5
7
  # including binding variables and escaping values.
6
8
  module SQLProcessor
7
9
  def process_sql
10
+ placeholders = @sql.scan(/(?<!\\)\?/).size
11
+ if placeholders != @bind_vars.size
12
+ raise ChDB::SQLException, "Wrong number of bind variables (#{@bind_vars.size} for #{placeholders})"
13
+ end
14
+
8
15
  escaped_values = @bind_vars.map { |v| escape(v) }
9
16
  sql = @sql.dup
10
17
  sql.gsub(/(?<!\\)\?/) { escaped_values.shift or '?' }
@@ -8,6 +8,7 @@ rescue LoadError
8
8
  require 'chdb/chdb_native'
9
9
  end
10
10
  require 'chdb/local_result'
11
+ require 'chdb/streaming_result'
11
12
  require 'chdb/result_set'
12
13
  require 'chdb/result_handler'
13
14
  require 'chdb/parameter_binding'
@@ -71,6 +72,21 @@ module ChDB
71
72
  @result.buf
72
73
  end
73
74
 
75
+ def send_query(*bind_vars, format)
76
+ reset! if @executed
77
+
78
+ bind_params(*bind_vars) unless bind_vars.empty?
79
+
80
+ my_processed_sql = process_sql
81
+ streaming_result = @connection.conn.send_query(my_processed_sql, format)
82
+ streaming_result.output_format = format
83
+
84
+ results = StreamingResultSet.new(@connection, streaming_result)
85
+
86
+ yield results if block_given?
87
+ results
88
+ end
89
+
74
90
  def reset!
75
91
  @executed = false
76
92
  @parsed = false
@@ -78,7 +94,7 @@ module ChDB
78
94
  @bind_vars.clear
79
95
  @parsed_data.clear
80
96
  @columns.clear
81
- @results = nil
97
+ @result = nil
82
98
  end
83
99
 
84
100
  def step
@@ -101,14 +117,14 @@ module ChDB
101
117
  end
102
118
 
103
119
  @parsed = true
104
- @results = nil
120
+ @result = nil
105
121
  end
106
122
 
107
123
  private
108
124
 
109
125
  def validate_inputs(db, sql_str)
110
- raise ArgumentError, 'SQL statement cannot be nil' if sql_str.nil?
111
- raise ArgumentError, 'prepare called on a closed database' if db.nil? || db.closed?
126
+ raise InvalidArgumentException, 'SQL statement cannot be nil' if sql_str.nil?
127
+ raise InvalidArgumentException, 'prepare called on a closed database' if db.nil? || db.closed?
112
128
  end
113
129
 
114
130
  def encode_sql(sql_str)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChDB
4
+ # Represents the local result of a ChDB operation.
5
+ class StreamingResult
6
+ attr_accessor :output_format
7
+ end
8
+ end
data/lib/chdb/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ChDB
4
4
  # (String) the version of the chdb gem, e.g. "0.1.0"
5
- VERSION = '0.1.0.rc.2'
5
+ VERSION = '0.2.0.rc.1'
6
6
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chdb-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.rc.2
4
+ version: 0.2.0.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xiaozhe Yu
8
8
  - Auxten Wang
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2025-04-01 00:00:00.000000000 Z
12
+ date: 2025-05-07 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: csv
@@ -27,6 +28,7 @@ dependencies:
27
28
  description: |
28
29
  Ruby library to interface with the chDB database engine (https://clickhouse.com/docs/chdb). Precompiled
29
30
  binaries are available for common platforms for recent versions of Ruby.
31
+ email:
30
32
  executables: []
31
33
  extensions:
32
34
  - ext/chdb/extconf.rb
@@ -41,12 +43,15 @@ files:
41
43
  - ext/chdb/chdb_handle.h
42
44
  - ext/chdb/connection.c
43
45
  - ext/chdb/connection.h
46
+ - ext/chdb/constants.c
44
47
  - ext/chdb/constants.h
45
48
  - ext/chdb/exception.c
46
49
  - ext/chdb/exception.h
47
50
  - ext/chdb/extconf.rb
48
51
  - ext/chdb/local_result.c
49
52
  - ext/chdb/local_result.h
53
+ - ext/chdb/streaming_result.c
54
+ - ext/chdb/streaming_result.h
50
55
  - lib/chdb.rb
51
56
  - lib/chdb/constants.rb
52
57
  - lib/chdb/data_path.rb
@@ -58,6 +63,7 @@ files:
58
63
  - lib/chdb/result_set.rb
59
64
  - lib/chdb/sql_processor.rb
60
65
  - lib/chdb/statement.rb
66
+ - lib/chdb/streaming_result.rb
61
67
  - lib/chdb/version.rb
62
68
  - lib/chdb/version_info.rb
63
69
  homepage: https://github.com/chdb-io/chdb-ruby
@@ -69,6 +75,7 @@ metadata:
69
75
  changelog_uri: https://github.com/chdb-io/chdb-ruby/blob/main/CHANGELOG.md
70
76
  source_code_uri: https://github.com/chdb-io/chdb-ruby
71
77
  rubygems_mfa_required: 'true'
78
+ post_install_message:
72
79
  rdoc_options:
73
80
  - "--main"
74
81
  - README.md
@@ -81,11 +88,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
81
88
  version: '3.1'
82
89
  required_rubygems_version: !ruby/object:Gem::Requirement
83
90
  requirements:
84
- - - ">="
91
+ - - ">"
85
92
  - !ruby/object:Gem::Version
86
- version: '0'
93
+ version: 1.3.1
87
94
  requirements: []
88
- rubygems_version: 3.6.2
95
+ rubygems_version: 3.3.27
96
+ signing_key:
89
97
  specification_version: 4
90
98
  summary: Ruby library to interface with the chDB database engine (https://clickhouse.com/docs/chdb).
91
99
  test_files: []