Skip to content

Implement global singleton pattern to ensure only one active database instance #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ ports/
tmp/
vendor/
test/
testdb/
testdb*/
ext/chdb/include/
ext/chdb/lib/
83 changes: 64 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,69 +20,111 @@ Note that this module is only compatible with ChDB 3.0.0 or newer.

## Quick start

``` ruby
Before using chdb-ruby, install the gem first. This will download the libchdb C++ library dependencies, so please be patient:
```bash
gem install chdb-ruby
```

Below are examples of common interfaces usage:

```ruby
require 'chdb'

# Open a database
db = ChDB::Database.new('test_db', results_as_hash: true)
# Parameter explanation:
# 1. path supports two formats:
# - ":memory:" in-memory temporary database (data destroyed on close)
# - "file:/path/to/db" file-based persistent database
# Configuration parameters can be appended via URL-style query (e.g. 'file:test.db?results_as_hash=true')
# 2. options hash supports:
# - results_as_hash: controls whether result sets return as hashes (default: arrays)
db = ChDB::Database.new('file:test.db', results_as_hash: true)

# Create a database
db.execute('CREATE DATABASE IF NOT EXISTS test')

# Create a table
db.execute('DROP TABLE IF EXISTS test.test_table')
rows = db.execute <<-SQL
CREATE TABLE test_table(
CREATE TABLE test.test_table(
id Int32,
name String)
ENGINE = MergeTree()
ORDER BY id);
ORDER BY id
SQL

# Execute a few inserts
{
1 => 'Alice',
2 => 'Bob'
1 => 'Alice',
2 => 'Bob'
}.each do |pair|
db.execute 'INSERT INTO test_table VALUES ( ?, ? )', pair
db.execute 'INSERT INTO test.test_table VALUES ( ?, ? )', pair
end

# Find a few rows
db.execute('SELECT * FROM test_table ORDER BY id') do |row|
db.execute('SELECT * FROM test.test_table ORDER BY id') do |row|
p row
end
# [{ 'id' => '1', 'name' => 'Alice' },
# { 'id' => '2', 'name' => 'Bob' }]

# When you need to open another database, you must first close the previous database
db.close()

# Open another database
db = ChDB::Database.new 'test2.db'
db = ChDB::Database.new 'file:test.db'

# Create another table
db.execute('DROP TABLE IF EXISTS test.test2_table')
rows = db.execute <<-SQL
CREATE TABLE test2_table(
CREATE TABLE test.test2_table(
id Int32,
name String)
ENGINE = MergeTree()
ORDER BY id");
ORDER BY id
SQL

# Execute inserts with parameter markers
db.execute('INSERT INTO test2_table (id, name)
db.execute('INSERT INTO test.test2_table (id, name)
VALUES (?, ?)', [3, 'Charlie'])

db.execute2('SELECT * FROM test2_table') do |row|
# Find rows with the first row displaying column names
db.execute2('SELECT * FROM test.test2_table') do |row|
p row
end
# [['id', 'name'], [3, 'Charlie']],
# ["id", "name"]
# ["3", "Charlie"]

# Close the database
db.close()

# Use ChDB::Database.open to automatically close the database connection:
ChDB::Database.open('file:test.db') do |db|
result = db.execute('SELECT 1')
p result.to_a # => [["1"]]
end

# Query with specific output formats (CSV, JSON, etc.):
# See more details at https://clickhouse.com/docs/interfaces/formats.
ChDB::Database.open(':memory:') do |db|
csv_data = db.query_with_format('SELECT 1 as a, 2 as b', 'CSV')
p csv_data
# "1,2\n"

json_data = db.query_with_format('SELECT 1 as a, 2 as b', 'JSON')
p json_data
end
```

## Thread Safety

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.

For example, the following code is fine because only the database
instance is shared among threads:
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.
**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.

```ruby
require 'chdb'

db = ChDB::Database.new ":memory:'
db = ChDB::Database.new ':memory:'

latch = Queue.new

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

p ts.map(&:value)
# [[["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]], [["1"]]]

db.close()
```

Other instances can be shared among threads, but they require that you provide
Expand Down
1 change: 1 addition & 0 deletions chdb.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Gem::Specification.new do |s|
'ext/chdb/connection.c',
'ext/chdb/connection.h',
'ext/chdb/constants.h',
'ext/chdb/constants.c',
'ext/chdb/exception.c',
'ext/chdb/exception.h',
'ext/chdb/extconf.rb',
Expand Down
11 changes: 0 additions & 11 deletions ext/chdb/chdb.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,6 @@
#include "exception.h"
#include "local_result.h"

void init_chdb_constants()
{
VALUE mChDB = rb_define_module("ChDB");
VALUE mChDBConstants = rb_define_module_under(mChDB, "Constants");
VALUE mmChDBOpen = rb_define_module_under(mChDBConstants, "Open");

rb_define_const(mmChDBOpen, "READONLY", INT2FIX(CHDB_OPEN_READONLY));
rb_define_const(mmChDBOpen, "READWRITE", INT2FIX(CHDB_OPEN_READWRITE));
rb_define_const(mmChDBOpen, "CREATE", INT2FIX(CHDB_OPEN_CREATE));
}

void Init_chdb_native()
{
DEBUG_PRINT("Initializing chdb extension");
Expand Down
34 changes: 27 additions & 7 deletions ext/chdb/chdb_handle.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,43 @@

#include <dlfcn.h>
#include <ruby.h>

#include "constants.h"
#include "exception.h"

void *chdb_handle = NULL;
connect_chdb_func connect_chdb_ptr = NULL;
close_conn_func close_conn_ptr = NULL;
query_conn_func query_conn_ptr = NULL;
free_result_v2_func free_result_v2_ptr = NULL;

VALUE get_chdb_rb_path(void)
VALUE get_chdb_rb_path()
{
VALUE chdb_module = rb_const_get(rb_cObject, rb_intern("ChDB"));
return rb_funcall(chdb_module, rb_intern("lib_file_path"), 0);
}

void close_chdb_handle()
{
if (chdb_handle)
{
dlclose(chdb_handle);
chdb_handle = NULL;
DEBUG_PRINT("Close chdb handle");
}
}

void init_chdb_handle()
{
VALUE rb_path = get_chdb_rb_path();
VALUE lib_dir = rb_file_dirname(rb_file_dirname(rb_path));
VALUE lib_path = rb_str_cat2(lib_dir, "/lib/chdb/lib/libchdb.so");
// printf("chdb.rb path from Ruby: %s\n", StringValueCStr(lib_path));

DEBUG_PRINT("chdb.rb path from Ruby: %s\n", StringValueCStr(lib_path));

connect_chdb_ptr = NULL;
close_conn_ptr = NULL;
query_conn_ptr = NULL;
free_result_v2_ptr = NULL;

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

if (!connect_chdb_ptr || !close_conn_ptr || !query_conn_ptr)
if (!connect_chdb_ptr || !close_conn_ptr || !query_conn_ptr || !free_result_v2_ptr)
{
rb_raise(cChDBError, "Symbol loading failed: %s\nMissing functions: connect_chdb(%p) close_conn(%p) query_conn(%p)",
close_chdb_handle();

rb_raise(cChDBError,
"Symbol loading failed: %s\nMissing functions: connect_chdb(%p) close_conn(%p) query_conn(%p), free_result_v2(%p)",
dlerror(),
(void*)connect_chdb_ptr,
(void*)close_conn_ptr,
(void*)query_conn_ptr);
(void*)query_conn_ptr,
(void*)free_result_v2_ptr);
}
}

rb_set_end_proc(close_chdb_handle, 0);
}
4 changes: 3 additions & 1 deletion ext/chdb/chdb_handle.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
typedef struct chdb_conn **(*connect_chdb_func)(int, char**);
typedef void (*close_conn_func)(struct chdb_conn**);
typedef struct local_result_v2 *(*query_conn_func)(struct chdb_conn*, const char*, const char*);
typedef void (*free_result_v2_func)(struct local_result_v2*);

extern connect_chdb_func connect_chdb_ptr;
extern close_conn_func close_conn_ptr;
extern query_conn_func query_conn_ptr;
extern free_result_v2_func free_result_v2_ptr;

extern void *chdb_handle;

void init_chdb_handle();

#endif
#endif
10 changes: 6 additions & 4 deletions ext/chdb/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
#include "include/chdb.h"
#include "local_result.h"

static void connection_free(void *ptr)
void connection_free(void *ptr)
{
Connection *conn = (Connection *)ptr;
DEBUG_PRINT("Closing connection: %p", (void*)conn->c_conn);
DEBUG_PRINT("Closing connection in connection_free: %p", (void*)conn->c_conn);

if (conn->c_conn)
{
close_conn_ptr(conn->c_conn);
Expand Down Expand Up @@ -67,7 +68,6 @@ VALUE connection_initialize(VALUE self, VALUE argc, VALUE argv)
}

xfree(c_argv);
rb_gc_unregister_address(&argv);
return self;
}

Expand All @@ -93,6 +93,7 @@ VALUE connection_query(VALUE self, VALUE query, VALUE format)
if (c_result->error_message)
{
VALUE error_message = rb_str_new_cstr(c_result->error_message);
free_result_v2_ptr(c_result);
rb_raise(cChDBError, "CHDB error: %s", StringValueCStr(error_message));
}

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

if (conn->c_conn)
if (conn && conn->c_conn)
{
close_conn_ptr(conn->c_conn);
conn->c_conn = NULL;
Expand Down
14 changes: 14 additions & 0 deletions ext/chdb/constants.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include "constants.h"

#include <ruby.h>

void init_chdb_constants()
{
VALUE mChDB = rb_define_module("ChDB");
VALUE mChDBConstants = rb_define_module_under(mChDB, "Constants");
VALUE mmChDBOpen = rb_define_module_under(mChDBConstants, "Open");

rb_define_const(mmChDBOpen, "READONLY", INT2FIX(CHDB_OPEN_READONLY));
rb_define_const(mmChDBOpen, "READWRITE", INT2FIX(CHDB_OPEN_READWRITE));
rb_define_const(mmChDBOpen, "CREATE", INT2FIX(CHDB_OPEN_CREATE));
}
2 changes: 2 additions & 0 deletions ext/chdb/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@
#define DEBUG_PRINT(fmt, ...) ((void)0)
#endif

void init_chdb_constants();

#endif
1 change: 1 addition & 0 deletions ext/chdb/exception.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ void init_exception()
else
{
cChDBError = rb_define_class_under(mChDB, "Exception", rb_eStandardError);
rb_global_variable(&cChDBError);
}
}
6 changes: 4 additions & 2 deletions ext/chdb/local_result.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

#include "constants.h"
#include "include/chdb.h"
#include "chdb_handle.h"

VALUE cLocalResult;

static void local_result_free(void *ptr)
void local_result_free(void *ptr)
{
LocalResult *result = (LocalResult *)ptr;
DEBUG_PRINT("Freeing LocalResult: %p", (void*)result);
if (result->c_result)
{
free_result_v2(result->c_result);
DEBUG_PRINT("Freeing local_result_v2: %p", (void*)result->c_result);
free_result_v2_ptr(result->c_result);
}
free(result);
}
Expand Down
Loading