Skip to content

Commit 988bbf6

Browse files
authored
Merge pull request #4 from chdb-io/dev
Implement global singleton pattern to ensure only one active database instance
2 parents 868277f + 228c0d8 commit 988bbf6

20 files changed

+670
-98
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ ports/
1818
tmp/
1919
vendor/
2020
test/
21-
testdb/
21+
testdb*/
2222
ext/chdb/include/
2323
ext/chdb/lib/

README.md

+64-19
Original file line numberDiff line numberDiff line change
@@ -20,69 +20,111 @@ Note that this module is only compatible with ChDB 3.0.0 or newer.
2020

2121
## Quick start
2222

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
2431
require 'chdb'
2532

2633
# 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')
2845

2946
# Create a table
47+
db.execute('DROP TABLE IF EXISTS test.test_table')
3048
rows = db.execute <<-SQL
31-
CREATE TABLE test_table(
49+
CREATE TABLE test.test_table(
3250
id Int32,
3351
name String)
3452
ENGINE = MergeTree()
35-
ORDER BY id);
53+
ORDER BY id
3654
SQL
3755

3856
# Execute a few inserts
3957
{
40-
1 => 'Alice',
41-
2 => 'Bob'
58+
1 => 'Alice',
59+
2 => 'Bob'
4260
}.each do |pair|
43-
db.execute 'INSERT INTO test_table VALUES ( ?, ? )', pair
61+
db.execute 'INSERT INTO test.test_table VALUES ( ?, ? )', pair
4462
end
4563

4664
# 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|
4866
p row
4967
end
5068
# [{ 'id' => '1', 'name' => 'Alice' },
5169
# { 'id' => '2', 'name' => 'Bob' }]
5270

71+
# When you need to open another database, you must first close the previous database
72+
db.close()
73+
5374
# Open another database
54-
db = ChDB::Database.new 'test2.db'
75+
db = ChDB::Database.new 'file:test.db'
5576

5677
# Create another table
78+
db.execute('DROP TABLE IF EXISTS test.test2_table')
5779
rows = db.execute <<-SQL
58-
CREATE TABLE test2_table(
80+
CREATE TABLE test.test2_table(
5981
id Int32,
6082
name String)
6183
ENGINE = MergeTree()
62-
ORDER BY id");
84+
ORDER BY id
6385
SQL
6486

6587
# Execute inserts with parameter markers
66-
db.execute('INSERT INTO test2_table (id, name)
88+
db.execute('INSERT INTO test.test2_table (id, name)
6789
VALUES (?, ?)', [3, 'Charlie'])
6890

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|
7093
p row
7194
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
73117
```
74118

75119
## Thread Safety
76120

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:
121+
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.
122+
**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.
81123

82124
```ruby
83125
require 'chdb'
84126

85-
db = ChDB::Database.new ":memory:'
127+
db = ChDB::Database.new ':memory:'
86128

87129
latch = Queue.new
88130

@@ -95,6 +137,9 @@ ts = 10.times.map {
95137
10.times { latch << nil }
96138

97139
p ts.map(&:value)
140+
# [[["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]]]
141+
142+
db.close()
98143
```
99144

100145
Other instances can be shared among threads, but they require that you provide

chdb.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Gem::Specification.new do |s|
4545
'ext/chdb/connection.c',
4646
'ext/chdb/connection.h',
4747
'ext/chdb/constants.h',
48+
'ext/chdb/constants.c',
4849
'ext/chdb/exception.c',
4950
'ext/chdb/exception.h',
5051
'ext/chdb/extconf.rb',

ext/chdb/chdb.c

-11
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@
77
#include "exception.h"
88
#include "local_result.h"
99

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-
}
20-
2110
void Init_chdb_native()
2211
{
2312
DEBUG_PRINT("Initializing chdb extension");

ext/chdb/chdb_handle.c

+27-7
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,43 @@
22

33
#include <dlfcn.h>
44
#include <ruby.h>
5-
5+
#include "constants.h"
66
#include "exception.h"
77

88
void *chdb_handle = NULL;
99
connect_chdb_func connect_chdb_ptr = NULL;
1010
close_conn_func close_conn_ptr = NULL;
1111
query_conn_func query_conn_ptr = NULL;
12+
free_result_v2_func free_result_v2_ptr = NULL;
1213

13-
VALUE get_chdb_rb_path(void)
14+
VALUE get_chdb_rb_path()
1415
{
1516
VALUE chdb_module = rb_const_get(rb_cObject, rb_intern("ChDB"));
1617
return rb_funcall(chdb_module, rb_intern("lib_file_path"), 0);
1718
}
1819

20+
void close_chdb_handle()
21+
{
22+
if (chdb_handle)
23+
{
24+
dlclose(chdb_handle);
25+
chdb_handle = NULL;
26+
DEBUG_PRINT("Close chdb handle");
27+
}
28+
}
29+
1930
void init_chdb_handle()
2031
{
2132
VALUE rb_path = get_chdb_rb_path();
2233
VALUE lib_dir = rb_file_dirname(rb_file_dirname(rb_path));
2334
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));
35+
36+
DEBUG_PRINT("chdb.rb path from Ruby: %s\n", StringValueCStr(lib_path));
2537

2638
connect_chdb_ptr = NULL;
2739
close_conn_ptr = NULL;
2840
query_conn_ptr = NULL;
41+
free_result_v2_ptr = NULL;
2942

3043
chdb_handle = dlopen(RSTRING_PTR(lib_path), RTLD_LAZY | RTLD_GLOBAL);
3144
if (!chdb_handle)
@@ -37,13 +50,20 @@ void init_chdb_handle()
3750
connect_chdb_ptr = (connect_chdb_func)dlsym(chdb_handle, "connect_chdb");
3851
close_conn_ptr = (close_conn_func)dlsym(chdb_handle, "close_conn");
3952
query_conn_ptr = (query_conn_func)dlsym(chdb_handle, "query_conn");
53+
free_result_v2_ptr = (free_result_v2_func)dlsym(chdb_handle, "free_result_v2");
4054

41-
if (!connect_chdb_ptr || !close_conn_ptr || !query_conn_ptr)
55+
if (!connect_chdb_ptr || !close_conn_ptr || !query_conn_ptr || !free_result_v2_ptr)
4256
{
43-
rb_raise(cChDBError, "Symbol loading failed: %s\nMissing functions: connect_chdb(%p) close_conn(%p) query_conn(%p)",
57+
close_chdb_handle();
58+
59+
rb_raise(cChDBError,
60+
"Symbol loading failed: %s\nMissing functions: connect_chdb(%p) close_conn(%p) query_conn(%p), free_result_v2(%p)",
4461
dlerror(),
4562
(void*)connect_chdb_ptr,
4663
(void*)close_conn_ptr,
47-
(void*)query_conn_ptr);
64+
(void*)query_conn_ptr,
65+
(void*)free_result_v2_ptr);
4866
}
49-
}
67+
68+
rb_set_end_proc(close_chdb_handle, 0);
69+
}

ext/chdb/chdb_handle.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
typedef struct chdb_conn **(*connect_chdb_func)(int, char**);
55
typedef void (*close_conn_func)(struct chdb_conn**);
66
typedef struct local_result_v2 *(*query_conn_func)(struct chdb_conn*, const char*, const char*);
7+
typedef void (*free_result_v2_func)(struct local_result_v2*);
78

89
extern connect_chdb_func connect_chdb_ptr;
910
extern close_conn_func close_conn_ptr;
1011
extern query_conn_func query_conn_ptr;
12+
extern free_result_v2_func free_result_v2_ptr;
1113

1214
extern void *chdb_handle;
1315

1416
void init_chdb_handle();
1517

16-
#endif
18+
#endif

ext/chdb/connection.c

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
#include "include/chdb.h"
77
#include "local_result.h"
88

9-
static void connection_free(void *ptr)
9+
void connection_free(void *ptr)
1010
{
1111
Connection *conn = (Connection *)ptr;
12-
DEBUG_PRINT("Closing connection: %p", (void*)conn->c_conn);
12+
DEBUG_PRINT("Closing connection in connection_free: %p", (void*)conn->c_conn);
13+
1314
if (conn->c_conn)
1415
{
1516
close_conn_ptr(conn->c_conn);
@@ -67,7 +68,6 @@ VALUE connection_initialize(VALUE self, VALUE argc, VALUE argv)
6768
}
6869

6970
xfree(c_argv);
70-
rb_gc_unregister_address(&argv);
7171
return self;
7272
}
7373

@@ -93,6 +93,7 @@ VALUE connection_query(VALUE self, VALUE query, VALUE format)
9393
if (c_result->error_message)
9494
{
9595
VALUE error_message = rb_str_new_cstr(c_result->error_message);
96+
free_result_v2_ptr(c_result);
9697
rb_raise(cChDBError, "CHDB error: %s", StringValueCStr(error_message));
9798
}
9899

@@ -108,8 +109,9 @@ VALUE connection_close(VALUE self)
108109
{
109110
Connection *conn;
110111
TypedData_Get_Struct(self, Connection, &ConnectionType, conn);
112+
DEBUG_PRINT("Closing connection in connection_close: %p", (void*)conn->c_conn);
111113

112-
if (conn->c_conn)
114+
if (conn && conn->c_conn)
113115
{
114116
close_conn_ptr(conn->c_conn);
115117
conn->c_conn = NULL;

ext/chdb/constants.c

+14
Original file line numberDiff line numberDiff line change
@@ -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+
}

ext/chdb/constants.h

+2
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@
1212
#define DEBUG_PRINT(fmt, ...) ((void)0)
1313
#endif
1414

15+
void init_chdb_constants();
16+
1517
#endif

ext/chdb/exception.c

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ void init_exception()
1212
else
1313
{
1414
cChDBError = rb_define_class_under(mChDB, "Exception", rb_eStandardError);
15+
rb_global_variable(&cChDBError);
1516
}
1617
}

ext/chdb/local_result.c

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22

33
#include "constants.h"
44
#include "include/chdb.h"
5+
#include "chdb_handle.h"
56

67
VALUE cLocalResult;
78

8-
static void local_result_free(void *ptr)
9+
void local_result_free(void *ptr)
910
{
1011
LocalResult *result = (LocalResult *)ptr;
1112
DEBUG_PRINT("Freeing LocalResult: %p", (void*)result);
1213
if (result->c_result)
1314
{
14-
free_result_v2(result->c_result);
15+
DEBUG_PRINT("Freeing local_result_v2: %p", (void*)result->c_result);
16+
free_result_v2_ptr(result->c_result);
1517
}
1618
free(result);
1719
}

0 commit comments

Comments
 (0)