libsql2 0.1.0

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.
@@ -0,0 +1,42 @@
1
+ use std::sync::OnceLock;
2
+
3
+ use magnus::{prelude::*, Error, ExceptionClass, RModule, Ruby};
4
+
5
+ /// Wrapper to allow ExceptionClass in OnceLock.
6
+ /// SAFETY: These are only accessed under the GVL (single-threaded Ruby execution).
7
+ #[derive(Debug)]
8
+ struct SyncExceptionClass(ExceptionClass);
9
+ unsafe impl Send for SyncExceptionClass {}
10
+ unsafe impl Sync for SyncExceptionClass {}
11
+
12
+ static ERROR_CLASS: OnceLock<SyncExceptionClass> = OnceLock::new();
13
+ static CLOSED_ERROR_CLASS: OnceLock<SyncExceptionClass> = OnceLock::new();
14
+
15
+ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
16
+ let error_class = module.define_error("Error", ruby.exception_runtime_error())?;
17
+ let closed_error_class = module.define_error("ClosedError", error_class)?;
18
+
19
+ ERROR_CLASS
20
+ .set(SyncExceptionClass(error_class))
21
+ .expect("error class already initialized");
22
+ CLOSED_ERROR_CLASS
23
+ .set(SyncExceptionClass(closed_error_class))
24
+ .expect("closed error class already initialized");
25
+
26
+ Ok(())
27
+ }
28
+
29
+ pub fn libsql_error() -> ExceptionClass {
30
+ ERROR_CLASS.get().expect("error class not initialized").0
31
+ }
32
+
33
+ pub fn closed_error() -> ExceptionClass {
34
+ CLOSED_ERROR_CLASS
35
+ .get()
36
+ .expect("closed error class not initialized")
37
+ .0
38
+ }
39
+
40
+ pub fn to_error(e: libsql::Error) -> Error {
41
+ Error::new(libsql_error(), e.to_string())
42
+ }
@@ -0,0 +1,20 @@
1
+ use magnus::{Error, Ruby};
2
+
3
+ mod connection;
4
+ mod database;
5
+ mod error;
6
+ mod rows;
7
+ mod statement;
8
+
9
+ #[magnus::init]
10
+ fn init(ruby: &Ruby) -> Result<(), Error> {
11
+ let module = ruby.define_module("Libsql")?;
12
+
13
+ error::init(ruby, &module)?;
14
+ database::init(ruby, &module)?;
15
+ connection::init(ruby, &module)?;
16
+ rows::init(ruby, &module)?;
17
+ statement::init(ruby, &module)?;
18
+
19
+ Ok(())
20
+ }
@@ -0,0 +1,154 @@
1
+ use std::sync::OnceLock;
2
+
3
+ use magnus::prelude::*;
4
+ use magnus::{Error, IntoValue, RArray, RClass, RHash, RModule, Ruby, Value};
5
+
6
+ use crate::error;
7
+
8
+ /// Wrapper to allow RClass in OnceLock.
9
+ /// SAFETY: Only accessed under the GVL (single-threaded Ruby execution).
10
+ #[derive(Debug)]
11
+ struct SyncRClass(RClass);
12
+ unsafe impl Send for SyncRClass {}
13
+ unsafe impl Sync for SyncRClass {}
14
+
15
+ static BLOB_CLASS: OnceLock<SyncRClass> = OnceLock::new();
16
+
17
+ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
18
+ let rows_class = module.define_class("Rows", ruby.class_object())?;
19
+ rows_class.define_method("columns", magnus::method!(Rows::columns, 0))?;
20
+ rows_class.define_method("types", magnus::method!(Rows::types, 0))?;
21
+ rows_class.define_method("each", magnus::method!(Rows::each, 0))?;
22
+ rows_class.define_method("to_a", magnus::method!(Rows::to_a, 0))?;
23
+ rows_class.include_module(ruby.module_enumerable())?;
24
+
25
+ let blob_class: RClass = ruby.eval("Libsql::Blob")?;
26
+ BLOB_CLASS
27
+ .set(SyncRClass(blob_class))
28
+ .expect("Blob class already initialized");
29
+
30
+ Ok(())
31
+ }
32
+
33
+ #[magnus::wrap(class = "Libsql::Rows", free_immediately, size)]
34
+ pub struct Rows {
35
+ column_names: Vec<String>,
36
+ column_types: Vec<Option<String>>,
37
+ data: Vec<Vec<CellValue>>,
38
+ }
39
+
40
+ pub fn blob_class() -> RClass {
41
+ BLOB_CLASS.get().expect("Blob class not initialized").0
42
+ }
43
+
44
+ /// Represents a cell value from a query result row.
45
+ pub enum CellValue {
46
+ Null,
47
+ Integer(i64),
48
+ Real(f64),
49
+ Text(String),
50
+ Blob(Vec<u8>),
51
+ }
52
+
53
+ impl From<libsql::Value> for CellValue {
54
+ fn from(val: libsql::Value) -> Self {
55
+ match val {
56
+ libsql::Value::Null => Self::Null,
57
+ libsql::Value::Integer(i) => Self::Integer(i),
58
+ libsql::Value::Real(f) => Self::Real(f),
59
+ libsql::Value::Text(s) => Self::Text(s),
60
+ libsql::Value::Blob(b) => Self::Blob(b),
61
+ }
62
+ }
63
+ }
64
+
65
+ impl Rows {
66
+ pub fn new(
67
+ column_names: Vec<String>,
68
+ column_types: Vec<Option<String>>,
69
+ data: Vec<Vec<CellValue>>,
70
+ ) -> Self {
71
+ Self {
72
+ column_names,
73
+ column_types,
74
+ data,
75
+ }
76
+ }
77
+
78
+ fn columns(&self) -> RArray {
79
+ let ruby = Ruby::get().expect("called from Ruby");
80
+ let ary = ruby.ary_new_capa(self.column_names.len());
81
+ for name in &self.column_names {
82
+ let _ = ary.push(ruby.str_new(name));
83
+ }
84
+ ary
85
+ }
86
+
87
+ fn types(&self) -> RArray {
88
+ let ruby = Ruby::get().expect("called from Ruby");
89
+ let ary = ruby.ary_new_capa(self.column_types.len());
90
+ for t in &self.column_types {
91
+ match t {
92
+ Some(s) => {
93
+ let _ = ary.push(ruby.str_new(s));
94
+ }
95
+ None => {
96
+ let _ = ary.push(ruby.qnil());
97
+ }
98
+ }
99
+ }
100
+ ary
101
+ }
102
+
103
+ fn each(&self) -> Result<Value, Error> {
104
+ let ruby = Ruby::get().expect("called from Ruby");
105
+ if !ruby.block_given() {
106
+ // TODO: return Enumerator when no block given
107
+ return Err(Error::new(error::libsql_error(), "no block given (yield)"));
108
+ }
109
+
110
+ for row_data in &self.data {
111
+ let hash = self.row_to_hash(&ruby, row_data)?;
112
+ let _: Value = ruby.yield_value(hash)?;
113
+ }
114
+
115
+ Ok(ruby.qnil().into_value_with(&ruby))
116
+ }
117
+
118
+ fn to_a(&self) -> Result<RArray, Error> {
119
+ let ruby = Ruby::get().expect("called from Ruby");
120
+ let ary = ruby.ary_new_capa(self.data.len());
121
+ for row_data in &self.data {
122
+ let hash = self.row_to_hash(&ruby, row_data)?;
123
+ let _ = ary.push(hash);
124
+ }
125
+ Ok(ary)
126
+ }
127
+
128
+ fn row_to_hash(&self, ruby: &Ruby, row_data: &[CellValue]) -> Result<RHash, Error> {
129
+ let hash = ruby.hash_new();
130
+ for (i, cell) in row_data.iter().enumerate() {
131
+ let key = ruby.str_new(&self.column_names[i]);
132
+ let val = cell_to_ruby_value(ruby, cell)?;
133
+ hash.aset(key, val)?;
134
+ }
135
+ Ok(hash)
136
+ }
137
+ }
138
+
139
+ fn cell_to_ruby_value(ruby: &Ruby, cell: &CellValue) -> Result<Value, Error> {
140
+ match cell {
141
+ CellValue::Null => Ok(ruby.qnil().into_value_with(ruby)),
142
+ CellValue::Integer(i) => Ok(ruby.into_value(*i)),
143
+ CellValue::Real(f) => Ok(ruby.into_value(*f)),
144
+ CellValue::Text(s) => {
145
+ let rstr = ruby.str_new(s);
146
+ Ok(rstr.into_value_with(ruby))
147
+ }
148
+ CellValue::Blob(bytes) => {
149
+ let rstr = ruby.str_from_slice(bytes);
150
+ let blob: Value = blob_class().funcall("new", (rstr,))?;
151
+ Ok(blob)
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,146 @@
1
+ use std::cell::RefCell;
2
+ use std::sync::Arc;
3
+
4
+ use magnus::{prelude::*, Error, RModule, Ruby, Value};
5
+ use tokio::runtime::Runtime;
6
+
7
+ use crate::connection::{params_to_libsql, ruby_array_to_params};
8
+ use crate::error;
9
+ use crate::rows::Rows;
10
+
11
+ pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
12
+ let class = module.define_class("Statement", ruby.class_object())?;
13
+ class.define_method("bind", magnus::method!(Statement::bind, 1))?;
14
+ class.define_method("execute", magnus::method!(Statement::execute, -1))?;
15
+ class.define_method("query", magnus::method!(Statement::query, -1))?;
16
+ class.define_method("reset", magnus::method!(Statement::reset, 0))?;
17
+ class.define_method("close", magnus::method!(Statement::close, 0))?;
18
+ class.define_method("closed?", magnus::method!(Statement::is_closed, 0))?;
19
+
20
+ Ok(())
21
+ }
22
+
23
+ /// Internal state dropped together on close to release file descriptors.
24
+ struct StatementInner {
25
+ rt: Arc<Runtime>,
26
+ stmt: libsql::Statement,
27
+ }
28
+
29
+ #[magnus::wrap(class = "Libsql::Statement", free_immediately, size)]
30
+ pub struct Statement {
31
+ inner: RefCell<Option<StatementInner>>,
32
+ column_names: Vec<String>,
33
+ column_types: Vec<Option<String>>,
34
+ bound_params: RefCell<Option<libsql::params::Params>>,
35
+ }
36
+
37
+ impl Statement {
38
+ pub fn new(
39
+ rt: Arc<Runtime>,
40
+ stmt: libsql::Statement,
41
+ column_names: Vec<String>,
42
+ column_types: Vec<Option<String>>,
43
+ ) -> Self {
44
+ Self {
45
+ inner: RefCell::new(Some(StatementInner { rt, stmt })),
46
+ column_names,
47
+ column_types,
48
+ bound_params: RefCell::new(None),
49
+ }
50
+ }
51
+
52
+ fn with_inner<F, T>(&self, f: F) -> Result<T, Error>
53
+ where
54
+ F: FnOnce(&StatementInner) -> Result<T, Error>,
55
+ {
56
+ let inner = self.inner.borrow();
57
+ let inner = inner
58
+ .as_ref()
59
+ .ok_or_else(|| Error::new(error::closed_error(), "statement is closed"))?;
60
+ f(inner)
61
+ }
62
+
63
+ fn bind(&self, params: Value) -> Result<(), Error> {
64
+ let param_values = ruby_array_to_params(&params)?;
65
+ let libsql_params = params_to_libsql(&param_values);
66
+ *self.bound_params.borrow_mut() = Some(libsql_params);
67
+ Ok(())
68
+ }
69
+
70
+ fn execute(&self, args: &[Value]) -> Result<u64, Error> {
71
+ let params = if args.is_empty() {
72
+ self.bound_params
73
+ .borrow_mut()
74
+ .take()
75
+ .unwrap_or(libsql::params::Params::None)
76
+ } else {
77
+ let param_values = ruby_array_to_params(&args[0])?;
78
+ params_to_libsql(&param_values)
79
+ };
80
+
81
+ self.with_inner(|inner| {
82
+ // Reset before re-execution to allow statement reuse
83
+ inner.stmt.reset();
84
+ inner
85
+ .rt
86
+ .block_on(async { inner.stmt.execute(params).await })
87
+ .map(|n| n as u64)
88
+ .map_err(error::to_error)
89
+ })
90
+ }
91
+
92
+ fn query(&self, args: &[Value]) -> Result<Rows, Error> {
93
+ let params = if args.is_empty() {
94
+ self.bound_params
95
+ .borrow_mut()
96
+ .take()
97
+ .unwrap_or(libsql::params::Params::None)
98
+ } else {
99
+ let param_values = ruby_array_to_params(&args[0])?;
100
+ params_to_libsql(&param_values)
101
+ };
102
+
103
+ self.with_inner(|inner| {
104
+ // Reset before re-execution to allow statement reuse
105
+ inner.stmt.reset();
106
+ inner.rt.block_on(async {
107
+ let mut rows = inner.stmt.query(params).await.map_err(error::to_error)?;
108
+
109
+ let col_count = self.column_names.len();
110
+ let mut data = Vec::new();
111
+ while let Some(row) = rows.next().await.map_err(error::to_error)? {
112
+ let mut row_data = Vec::with_capacity(col_count);
113
+ for i in 0..col_count {
114
+ let val = row.get_value(i as i32).map_err(error::to_error)?;
115
+ row_data.push(val.into());
116
+ }
117
+ data.push(row_data);
118
+ }
119
+
120
+ Ok(Rows::new(
121
+ self.column_names.clone(),
122
+ self.column_types.clone(),
123
+ data,
124
+ ))
125
+ })
126
+ })
127
+ }
128
+
129
+ fn reset(&self) -> Result<(), Error> {
130
+ self.with_inner(|inner| {
131
+ inner.stmt.reset();
132
+ Ok(())
133
+ })?;
134
+ *self.bound_params.borrow_mut() = None;
135
+ Ok(())
136
+ }
137
+
138
+ fn close(&self) -> Result<(), Error> {
139
+ let _ = self.inner.borrow_mut().take();
140
+ Ok(())
141
+ }
142
+
143
+ fn is_closed(&self) -> bool {
144
+ self.inner.borrow().is_none()
145
+ }
146
+ }
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libsql
4
+ class Blob < String
5
+ def initialize(data = "")
6
+ super
7
+ force_encoding(Encoding::ASCII_8BIT)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libsql
4
+ class Connection
5
+ def transaction
6
+ execute("BEGIN")
7
+ result = yield self
8
+ execute("COMMIT")
9
+ result
10
+ rescue Exception # rubocop:disable Lint/RescueException
11
+ execute("ROLLBACK")
12
+ raise
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libsql
4
+ class Database
5
+ class << self
6
+ alias _open open
7
+
8
+ def open(path, **opts)
9
+ db = opts.empty? ? _open(path) : _open(path, opts)
10
+ if block_given?
11
+ begin
12
+ yield db
13
+ ensure
14
+ db.close
15
+ end
16
+ else
17
+ db
18
+ end
19
+ end
20
+ end
21
+
22
+ alias _connect connect
23
+
24
+ def connect
25
+ conn = _connect
26
+ if block_given?
27
+ begin
28
+ yield conn
29
+ ensure
30
+ conn.close
31
+ end
32
+ else
33
+ conn
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libsql
4
+ VERSION = "0.1.0"
5
+ end
data/lib/libsql2.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "libsql/version"
4
+ require_relative "libsql/blob"
5
+ require_relative "libsql/libsql2"
6
+ require_relative "libsql/connection"
7
+ require_relative "libsql/database"
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: libsql2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - speria-jp
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ description: Native Ruby bindings for libSQL database, built with Rust (magnus + libsql
27
+ crate)
28
+ executables: []
29
+ extensions:
30
+ - ext/libsql2/extconf.rb
31
+ extra_rdoc_files: []
32
+ files:
33
+ - Cargo.lock
34
+ - Cargo.toml
35
+ - LICENSE
36
+ - README.md
37
+ - ext/libsql2/Cargo.toml
38
+ - ext/libsql2/extconf.rb
39
+ - ext/libsql2/src/connection.rs
40
+ - ext/libsql2/src/database.rs
41
+ - ext/libsql2/src/error.rs
42
+ - ext/libsql2/src/lib.rs
43
+ - ext/libsql2/src/rows.rs
44
+ - ext/libsql2/src/statement.rs
45
+ - lib/libsql/blob.rb
46
+ - lib/libsql/connection.rb
47
+ - lib/libsql/database.rb
48
+ - lib/libsql/version.rb
49
+ - lib/libsql2.rb
50
+ homepage: https://github.com/speria-jp/libsql-ruby2
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ rubygems_mfa_required: 'true'
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '3.4'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 4.0.3
70
+ specification_version: 4
71
+ summary: Ruby bindings for libSQL using libsql crate + magnus
72
+ test_files: []