stoolap 0.4.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.
- checksums.yaml +7 -0
- data/Cargo.lock +1792 -0
- data/Cargo.toml +9 -0
- data/LICENSE +201 -0
- data/README.md +477 -0
- data/ext/stoolap/Cargo.toml +20 -0
- data/ext/stoolap/extconf.rb +6 -0
- data/ext/stoolap/src/database.rs +432 -0
- data/ext/stoolap/src/error.rs +53 -0
- data/ext/stoolap/src/lib.rs +103 -0
- data/ext/stoolap/src/statement.rs +193 -0
- data/ext/stoolap/src/transaction.rs +246 -0
- data/ext/stoolap/src/value.rs +296 -0
- data/lib/stoolap/version.rb +5 -0
- data/lib/stoolap.rb +86 -0
- metadata +145 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "stoolap"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
authors = ["Stoolap Contributors"]
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
description = "Native Ruby driver for the Stoolap embedded SQL database"
|
|
8
|
+
repository = "https://github.com/stoolap/stoolap-ruby"
|
|
9
|
+
homepage = "https://stoolap.io"
|
|
10
|
+
publish = false
|
|
11
|
+
|
|
12
|
+
[lib]
|
|
13
|
+
name = "stoolap"
|
|
14
|
+
crate-type = ["cdylib"]
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
stoolap = "0.4.0"
|
|
18
|
+
magnus = "0.8"
|
|
19
|
+
chrono = "0.4"
|
|
20
|
+
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// Copyright 2025 Stoolap Contributors
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
use std::sync::Arc;
|
|
16
|
+
|
|
17
|
+
use magnus::{prelude::*, scan_args::scan_args, Error, RArray, RHash, Ruby, Value};
|
|
18
|
+
|
|
19
|
+
use stoolap::api::{Database as ApiDatabase, Rows};
|
|
20
|
+
|
|
21
|
+
use crate::error::{raise, to_magnus};
|
|
22
|
+
use crate::statement::PreparedStatement;
|
|
23
|
+
use crate::transaction::Transaction;
|
|
24
|
+
use crate::value::{parse_params, value_to_ruby, BindParams};
|
|
25
|
+
|
|
26
|
+
/// A Stoolap database connection.
|
|
27
|
+
///
|
|
28
|
+
/// Open with `Stoolap::Database.open(path)`. Use `:memory:` for in-memory.
|
|
29
|
+
#[magnus::wrap(class = "Stoolap::Database", free_immediately, size)]
|
|
30
|
+
pub struct Database {
|
|
31
|
+
pub(crate) db: Arc<ApiDatabase>,
|
|
32
|
+
closed: std::sync::atomic::AtomicBool,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl Database {
|
|
36
|
+
/// Open a database connection.
|
|
37
|
+
///
|
|
38
|
+
/// Accepts:
|
|
39
|
+
/// - `:memory:` or empty string for in-memory database
|
|
40
|
+
/// - `memory://` for in-memory database
|
|
41
|
+
/// - `./mydb` or `file:///path/to/db` for file-based database
|
|
42
|
+
pub fn open(path: String) -> Result<Self, Error> {
|
|
43
|
+
let dsn = translate_path(&path);
|
|
44
|
+
let db = ApiDatabase::open(&dsn).map_err(to_magnus)?;
|
|
45
|
+
Ok(Self {
|
|
46
|
+
db: Arc::new(db),
|
|
47
|
+
closed: std::sync::atomic::AtomicBool::new(false),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Execute a DDL/DML statement. Returns rows affected.
|
|
52
|
+
pub fn execute(&self, args: &[Value]) -> Result<i64, Error> {
|
|
53
|
+
let (sql, params) = parse_sql_args(args)?;
|
|
54
|
+
let bind = parse_params(params)?;
|
|
55
|
+
match bind {
|
|
56
|
+
BindParams::Positional(p) => self.db.execute(&sql, p).map_err(to_magnus),
|
|
57
|
+
BindParams::Named(n) => self.db.execute_named(&sql, n).map_err(to_magnus),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Execute one or more SQL statements separated by semicolons (no params).
|
|
62
|
+
pub fn exec(&self, sql: String) -> Result<(), Error> {
|
|
63
|
+
for stmt in SqlSplitter::new(&sql) {
|
|
64
|
+
let trimmed = stmt.trim();
|
|
65
|
+
if trimmed.is_empty() {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
self.db.execute(trimmed, ()).map_err(to_magnus)?;
|
|
69
|
+
}
|
|
70
|
+
Ok(())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Query rows. Returns an Array of Hashes (column name => value).
|
|
74
|
+
pub fn query(&self, args: &[Value]) -> Result<RArray, Error> {
|
|
75
|
+
let (sql, params) = parse_sql_args(args)?;
|
|
76
|
+
let bind = parse_params(params)?;
|
|
77
|
+
let rows = match bind {
|
|
78
|
+
BindParams::Positional(p) => self.db.query(&sql, p).map_err(to_magnus)?,
|
|
79
|
+
BindParams::Named(n) => self.db.query_named(&sql, n).map_err(to_magnus)?,
|
|
80
|
+
};
|
|
81
|
+
rows_to_hashes(rows)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Query a single row. Returns a Hash, or nil if no rows.
|
|
85
|
+
pub fn query_one(&self, args: &[Value]) -> Result<Value, Error> {
|
|
86
|
+
let (sql, params) = parse_sql_args(args)?;
|
|
87
|
+
let bind = parse_params(params)?;
|
|
88
|
+
let rows = match bind {
|
|
89
|
+
BindParams::Positional(p) => self.db.query(&sql, p).map_err(to_magnus)?,
|
|
90
|
+
BindParams::Named(n) => self.db.query_named(&sql, n).map_err(to_magnus)?,
|
|
91
|
+
};
|
|
92
|
+
first_row_to_hash(rows)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Query rows in raw columnar format.
|
|
96
|
+
/// Returns a Hash with `"columns"` (Array of String) and `"rows"` (Array of Arrays).
|
|
97
|
+
pub fn query_raw(&self, args: &[Value]) -> Result<RHash, Error> {
|
|
98
|
+
let (sql, params) = parse_sql_args(args)?;
|
|
99
|
+
let bind = parse_params(params)?;
|
|
100
|
+
let rows = match bind {
|
|
101
|
+
BindParams::Positional(p) => self.db.query(&sql, p).map_err(to_magnus)?,
|
|
102
|
+
BindParams::Named(n) => self.db.query_named(&sql, n).map_err(to_magnus)?,
|
|
103
|
+
};
|
|
104
|
+
rows_to_raw(rows)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Execute the same SQL with multiple parameter sets, auto-wrapped in a transaction.
|
|
108
|
+
/// Returns total rows affected.
|
|
109
|
+
pub fn execute_batch(&self, sql: String, params_list: RArray) -> Result<i64, Error> {
|
|
110
|
+
use stoolap::api::ParamVec;
|
|
111
|
+
use stoolap::parser::Parser;
|
|
112
|
+
|
|
113
|
+
let mut all_params: Vec<ParamVec> = Vec::with_capacity(params_list.len());
|
|
114
|
+
for item in params_list.into_iter() {
|
|
115
|
+
match parse_params(Some(item))? {
|
|
116
|
+
BindParams::Positional(p) => all_params.push(p),
|
|
117
|
+
BindParams::Named(_) => {
|
|
118
|
+
return Err(raise(
|
|
119
|
+
"execute_batch only supports positional parameters (Array)",
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let mut parser = Parser::new(&sql);
|
|
126
|
+
let program = parser.parse_program().map_err(|e| raise(e.to_string()))?;
|
|
127
|
+
if program.statements.len() > 1 {
|
|
128
|
+
return Err(raise(
|
|
129
|
+
"execute_batch accepts exactly one SQL statement; use exec() for multi-statement SQL",
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
let stmt = program
|
|
133
|
+
.statements
|
|
134
|
+
.first()
|
|
135
|
+
.ok_or_else(|| raise("No SQL statement found"))?;
|
|
136
|
+
|
|
137
|
+
let mut tx = self.db.begin().map_err(to_magnus)?;
|
|
138
|
+
let mut total = 0i64;
|
|
139
|
+
for params in all_params {
|
|
140
|
+
total += tx.execute_prepared(stmt, params).map_err(to_magnus)?;
|
|
141
|
+
}
|
|
142
|
+
tx.commit().map_err(to_magnus)?;
|
|
143
|
+
Ok(total)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Create a prepared statement.
|
|
147
|
+
pub fn prepare(&self, sql: String) -> Result<PreparedStatement, Error> {
|
|
148
|
+
PreparedStatement::new(Arc::clone(&self.db), &sql)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Begin a transaction.
|
|
152
|
+
pub fn begin_transaction(&self) -> Result<Transaction, Error> {
|
|
153
|
+
let tx = self.db.begin().map_err(to_magnus)?;
|
|
154
|
+
Ok(Transaction::from_tx(tx))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Close the database connection.
|
|
158
|
+
pub fn close(&self) -> Result<(), Error> {
|
|
159
|
+
self.closed
|
|
160
|
+
.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
161
|
+
self.db.close().map_err(to_magnus)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pub fn inspect(&self) -> String {
|
|
165
|
+
if self.closed.load(std::sync::atomic::Ordering::Relaxed) {
|
|
166
|
+
"#<Stoolap::Database closed>".to_string()
|
|
167
|
+
} else {
|
|
168
|
+
"#<Stoolap::Database open>".to_string()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Parse `(sql, params=nil)` from a method args slice.
|
|
174
|
+
fn parse_sql_args(args: &[Value]) -> Result<(String, Option<Value>), Error> {
|
|
175
|
+
let scanned = scan_args::<(String,), (Option<Value>,), (), (), (), ()>(args)?;
|
|
176
|
+
Ok((scanned.required.0, scanned.optional.0))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Translate user-friendly paths into a Stoolap DSN.
|
|
180
|
+
fn translate_path(path: &str) -> String {
|
|
181
|
+
let trimmed = path.trim();
|
|
182
|
+
if trimmed.is_empty() || trimmed == ":memory:" {
|
|
183
|
+
"memory://".to_string()
|
|
184
|
+
} else if trimmed.starts_with("memory://") || trimmed.starts_with("file://") {
|
|
185
|
+
trimmed.to_string()
|
|
186
|
+
} else {
|
|
187
|
+
format!("file://{trimmed}")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Convert a `Rows` iterator into an Array of Hashes (String keys).
|
|
192
|
+
///
|
|
193
|
+
/// Uses `rows.advance() + rows.current_row()` which yields `&Row` directly
|
|
194
|
+
/// with zero `take_row()` move and zero ResultRow wrapping per iteration.
|
|
195
|
+
/// Column-name strings are built once into a Ruby `RArray` held as a stack
|
|
196
|
+
/// local, so Ruby's conservative GC scanner can find the `RArray` pointer
|
|
197
|
+
/// on the C stack and trace through to mark every column `RString`. A
|
|
198
|
+
/// `Vec<RString>` would store the keys in a heap-allocated buffer that
|
|
199
|
+
/// Ruby's GC cannot see, and the strings could be collected mid-iteration,
|
|
200
|
+
/// segfaulting on the next `aset`.
|
|
201
|
+
pub fn rows_to_hashes(rows: Rows) -> Result<RArray, Error> {
|
|
202
|
+
let ruby = Ruby::get().expect("must hold the Ruby VM lock");
|
|
203
|
+
let col_count = rows.columns().len();
|
|
204
|
+
let key_cache = ruby.ary_new_capa(col_count);
|
|
205
|
+
for c in rows.columns() {
|
|
206
|
+
let s = ruby.str_new(c);
|
|
207
|
+
s.freeze();
|
|
208
|
+
key_cache.push(s)?;
|
|
209
|
+
}
|
|
210
|
+
rows_to_hashes_with_keys(&ruby, rows, key_cache)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Same as `rows_to_hashes` but reuses a caller-provided key cache
|
|
214
|
+
/// (typically held on a `PreparedStatement` across calls).
|
|
215
|
+
pub fn rows_to_hashes_with_keys(
|
|
216
|
+
ruby: &Ruby,
|
|
217
|
+
mut rows: Rows,
|
|
218
|
+
key_cache: RArray,
|
|
219
|
+
) -> Result<RArray, Error> {
|
|
220
|
+
let col_count = rows.columns().len();
|
|
221
|
+
let result = ruby.ary_new();
|
|
222
|
+
while rows.advance() {
|
|
223
|
+
let row = rows.current_row();
|
|
224
|
+
let hash = ruby.hash_new();
|
|
225
|
+
for i in 0..col_count {
|
|
226
|
+
let key = key_cache.entry::<Value>(i as isize)?;
|
|
227
|
+
let val = match row.get(i) {
|
|
228
|
+
Some(v) => value_to_ruby(v)?,
|
|
229
|
+
None => ruby.qnil().as_value(),
|
|
230
|
+
};
|
|
231
|
+
hash.aset(key, val)?;
|
|
232
|
+
}
|
|
233
|
+
result.push(hash)?;
|
|
234
|
+
}
|
|
235
|
+
if let Some(err) = rows.error() {
|
|
236
|
+
return Err(to_magnus(err));
|
|
237
|
+
}
|
|
238
|
+
Ok(result)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Take the first row from `Rows` as a Hash, or `nil`.
|
|
242
|
+
pub fn first_row_to_hash(rows: Rows) -> Result<Value, Error> {
|
|
243
|
+
let ruby = Ruby::get().expect("must hold the Ruby VM lock");
|
|
244
|
+
let col_count = rows.columns().len();
|
|
245
|
+
let key_cache = ruby.ary_new_capa(col_count);
|
|
246
|
+
for c in rows.columns() {
|
|
247
|
+
let s = ruby.str_new(c);
|
|
248
|
+
s.freeze();
|
|
249
|
+
key_cache.push(s)?;
|
|
250
|
+
}
|
|
251
|
+
first_row_to_hash_with_keys(&ruby, rows, key_cache)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Same as `first_row_to_hash` but with a caller-provided key cache.
|
|
255
|
+
pub fn first_row_to_hash_with_keys(
|
|
256
|
+
ruby: &Ruby,
|
|
257
|
+
mut rows: Rows,
|
|
258
|
+
key_cache: RArray,
|
|
259
|
+
) -> Result<Value, Error> {
|
|
260
|
+
if rows.advance() {
|
|
261
|
+
let col_count = rows.columns().len();
|
|
262
|
+
let row = rows.current_row();
|
|
263
|
+
let hash = ruby.hash_new();
|
|
264
|
+
for i in 0..col_count {
|
|
265
|
+
let key = key_cache.entry::<Value>(i as isize)?;
|
|
266
|
+
let val = match row.get(i) {
|
|
267
|
+
Some(v) => value_to_ruby(v)?,
|
|
268
|
+
None => ruby.qnil().as_value(),
|
|
269
|
+
};
|
|
270
|
+
hash.aset(key, val)?;
|
|
271
|
+
}
|
|
272
|
+
return Ok(hash.as_value());
|
|
273
|
+
}
|
|
274
|
+
if let Some(err) = rows.error() {
|
|
275
|
+
return Err(to_magnus(err));
|
|
276
|
+
}
|
|
277
|
+
Ok(ruby.qnil().as_value())
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// Convert `Rows` into `{ "columns" => [..], "rows" => [[..], ..] }`.
|
|
281
|
+
pub fn rows_to_raw(rows: Rows) -> Result<RHash, Error> {
|
|
282
|
+
let ruby = Ruby::get().expect("must hold the Ruby VM lock");
|
|
283
|
+
let col_count = rows.columns().len();
|
|
284
|
+
let col_arr = ruby.ary_new_capa(col_count);
|
|
285
|
+
for c in rows.columns() {
|
|
286
|
+
col_arr.push(ruby.str_new(c))?;
|
|
287
|
+
}
|
|
288
|
+
rows_to_raw_with_keys(&ruby, rows, col_arr)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Same as `rows_to_raw` but with a caller-provided column-name array.
|
|
292
|
+
///
|
|
293
|
+
/// If the caller passes its own internal cache (e.g. `PreparedStatement`),
|
|
294
|
+
/// the cache is frozen and we must `.dup()` it before inserting into the
|
|
295
|
+
/// result hash so the user cannot mutate the cache through the returned
|
|
296
|
+
/// hash.
|
|
297
|
+
pub fn rows_to_raw_with_keys(ruby: &Ruby, mut rows: Rows, col_arr: RArray) -> Result<RHash, Error> {
|
|
298
|
+
let col_count = rows.columns().len();
|
|
299
|
+
let row_arr = ruby.ary_new();
|
|
300
|
+
while rows.advance() {
|
|
301
|
+
let row = rows.current_row();
|
|
302
|
+
let inner = ruby.ary_new_capa(col_count);
|
|
303
|
+
for i in 0..col_count {
|
|
304
|
+
let val = match row.get(i) {
|
|
305
|
+
Some(v) => value_to_ruby(v)?,
|
|
306
|
+
None => ruby.qnil().as_value(),
|
|
307
|
+
};
|
|
308
|
+
inner.push(val)?;
|
|
309
|
+
}
|
|
310
|
+
row_arr.push(inner)?;
|
|
311
|
+
}
|
|
312
|
+
if let Some(err) = rows.error() {
|
|
313
|
+
return Err(to_magnus(err));
|
|
314
|
+
}
|
|
315
|
+
// If the column array is frozen (PreparedStatement cache), return a
|
|
316
|
+
// mutable copy so user code can safely mutate the result hash without
|
|
317
|
+
// corrupting the statement's internal cache.
|
|
318
|
+
let columns_for_result: Value = if col_arr.is_frozen() {
|
|
319
|
+
col_arr.funcall("dup", ())?
|
|
320
|
+
} else {
|
|
321
|
+
col_arr.as_value()
|
|
322
|
+
};
|
|
323
|
+
let hash = ruby.hash_new();
|
|
324
|
+
hash.aset("columns", columns_for_result)?;
|
|
325
|
+
hash.aset("rows", row_arr)?;
|
|
326
|
+
Ok(hash)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// Iterator that splits SQL on unquoted, uncommented semicolons and yields
|
|
330
|
+
/// `&str` slices borrowed from the input. Zero heap allocation per call,
|
|
331
|
+
/// in contrast with the previous `Vec<char>` + `Vec<String>` implementation.
|
|
332
|
+
struct SqlSplitter<'a> {
|
|
333
|
+
bytes: &'a [u8],
|
|
334
|
+
src: &'a str,
|
|
335
|
+
cursor: usize,
|
|
336
|
+
done: bool,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
impl<'a> SqlSplitter<'a> {
|
|
340
|
+
fn new(src: &'a str) -> Self {
|
|
341
|
+
Self {
|
|
342
|
+
bytes: src.as_bytes(),
|
|
343
|
+
src,
|
|
344
|
+
cursor: 0,
|
|
345
|
+
done: false,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
impl<'a> Iterator for SqlSplitter<'a> {
|
|
351
|
+
type Item = &'a str;
|
|
352
|
+
|
|
353
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
354
|
+
if self.done {
|
|
355
|
+
return None;
|
|
356
|
+
}
|
|
357
|
+
let start = self.cursor;
|
|
358
|
+
let len = self.bytes.len();
|
|
359
|
+
let mut i = start;
|
|
360
|
+
let mut in_single = false;
|
|
361
|
+
let mut in_double = false;
|
|
362
|
+
let mut in_line_comment = false;
|
|
363
|
+
let mut in_block_comment = false;
|
|
364
|
+
|
|
365
|
+
while i < len {
|
|
366
|
+
let c = self.bytes[i];
|
|
367
|
+
|
|
368
|
+
if in_line_comment {
|
|
369
|
+
if c == b'\n' {
|
|
370
|
+
in_line_comment = false;
|
|
371
|
+
}
|
|
372
|
+
i += 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if in_block_comment {
|
|
377
|
+
if c == b'*' && i + 1 < len && self.bytes[i + 1] == b'/' {
|
|
378
|
+
in_block_comment = false;
|
|
379
|
+
i += 2;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
i += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Line comment: `-- ` or `--\t` or `--\n` or `--` at EOF.
|
|
387
|
+
// Matches stoolap's lexer which treats `--identifier` as double
|
|
388
|
+
// negation (not a comment) to support `SELECT --val FROM t`.
|
|
389
|
+
if !in_single && !in_double && c == b'-' && i + 1 < len && self.bytes[i + 1] == b'-' {
|
|
390
|
+
let next = if i + 2 < len { self.bytes[i + 2] } else { 0 };
|
|
391
|
+
if next == 0 || next == b' ' || next == b'\t' || next == b'\n' || next == b'\r' {
|
|
392
|
+
in_line_comment = true;
|
|
393
|
+
i += 2;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Block comment start.
|
|
399
|
+
if !in_single && !in_double && c == b'/' && i + 1 < len && self.bytes[i + 1] == b'*' {
|
|
400
|
+
in_block_comment = true;
|
|
401
|
+
i += 2;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Quote toggles (respecting `\` escape).
|
|
406
|
+
if c == b'\'' && !in_double && (i == 0 || self.bytes[i - 1] != b'\\') {
|
|
407
|
+
in_single = !in_single;
|
|
408
|
+
} else if c == b'"' && !in_single && (i == 0 || self.bytes[i - 1] != b'\\') {
|
|
409
|
+
in_double = !in_double;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Statement terminator.
|
|
413
|
+
if c == b';' && !in_single && !in_double {
|
|
414
|
+
// Input is valid UTF-8 and we only stop at ASCII `;`, so
|
|
415
|
+
// byte-indexed slicing is always on a char boundary.
|
|
416
|
+
let stmt = &self.src[start..i];
|
|
417
|
+
self.cursor = i + 1;
|
|
418
|
+
return Some(stmt);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
i += 1;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Tail (no trailing semicolon).
|
|
425
|
+
self.done = true;
|
|
426
|
+
if start >= len {
|
|
427
|
+
None
|
|
428
|
+
} else {
|
|
429
|
+
Some(&self.src[start..])
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Copyright 2025 Stoolap Contributors
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
use magnus::{
|
|
16
|
+
exception::ExceptionClass,
|
|
17
|
+
prelude::*,
|
|
18
|
+
value::{Lazy, ReprValue},
|
|
19
|
+
Error, RClass, RModule, Ruby,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/// Lazily-resolved `Stoolap::Error` class. The class itself is created in
|
|
23
|
+
/// `init()` (`module.define_error`) so by the time any binding method runs,
|
|
24
|
+
/// the constant is guaranteed to exist.
|
|
25
|
+
static ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
26
|
+
let module: RModule = ruby
|
|
27
|
+
.class_object()
|
|
28
|
+
.const_get("Stoolap")
|
|
29
|
+
.expect("Stoolap module must exist before Error class is resolved");
|
|
30
|
+
let class: RClass = module
|
|
31
|
+
.const_get("Error")
|
|
32
|
+
.expect("Stoolap::Error must be defined during init");
|
|
33
|
+
// RClass -> ExceptionClass: Stoolap::Error is a subclass of StandardError.
|
|
34
|
+
ExceptionClass::from_value(class.as_value()).expect("Stoolap::Error must be an exception class")
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/// Build a Magnus `Error` from a Stoolap engine error.
|
|
38
|
+
pub fn to_magnus(err: stoolap::Error) -> Error {
|
|
39
|
+
let ruby = Ruby::get().expect("must hold the Ruby VM lock");
|
|
40
|
+
Error::new(ruby.get_inner(&ERROR_CLASS), err.to_string())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Build a Magnus `Error` from a free-form message.
|
|
44
|
+
pub fn raise<S: Into<String>>(msg: S) -> Error {
|
|
45
|
+
let ruby = Ruby::get().expect("must hold the Ruby VM lock");
|
|
46
|
+
Error::new(ruby.get_inner(&ERROR_CLASS), msg.into())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Build a `TypeError` for invalid Ruby types.
|
|
50
|
+
pub fn type_error<S: Into<String>>(msg: S) -> Error {
|
|
51
|
+
let ruby = Ruby::get().expect("must hold the Ruby VM lock");
|
|
52
|
+
Error::new(ruby.exception_type_error(), msg.into())
|
|
53
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright 2025 Stoolap Contributors
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
mod database;
|
|
16
|
+
mod error;
|
|
17
|
+
mod statement;
|
|
18
|
+
mod transaction;
|
|
19
|
+
mod value;
|
|
20
|
+
|
|
21
|
+
use magnus::{function, method, prelude::*, Error, Ruby};
|
|
22
|
+
|
|
23
|
+
use crate::database::Database;
|
|
24
|
+
use crate::statement::PreparedStatement;
|
|
25
|
+
use crate::transaction::Transaction;
|
|
26
|
+
use crate::value::Vector;
|
|
27
|
+
|
|
28
|
+
/// Native Stoolap database bindings for Ruby.
|
|
29
|
+
///
|
|
30
|
+
/// Defines the `Stoolap` module with `Database`, `Transaction`,
|
|
31
|
+
/// `PreparedStatement`, `Vector`, and `Error` classes.
|
|
32
|
+
#[magnus::init]
|
|
33
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
34
|
+
let module = ruby.define_module("Stoolap")?;
|
|
35
|
+
|
|
36
|
+
// Custom exception class. Subclass of StandardError, looked up lazily by error.rs.
|
|
37
|
+
module.define_error("Error", ruby.exception_standard_error())?;
|
|
38
|
+
|
|
39
|
+
// Database
|
|
40
|
+
let db_class = module.define_class("Database", ruby.class_object())?;
|
|
41
|
+
db_class.define_singleton_method("_open", function!(Database::open, 1))?;
|
|
42
|
+
db_class.define_method("execute", method!(Database::execute, -1))?;
|
|
43
|
+
db_class.define_method("exec", method!(Database::exec, 1))?;
|
|
44
|
+
db_class.define_method("query", method!(Database::query, -1))?;
|
|
45
|
+
db_class.define_method("query_one", method!(Database::query_one, -1))?;
|
|
46
|
+
db_class.define_method("query_raw", method!(Database::query_raw, -1))?;
|
|
47
|
+
db_class.define_method("execute_batch", method!(Database::execute_batch, 2))?;
|
|
48
|
+
db_class.define_method("prepare", method!(Database::prepare, 1))?;
|
|
49
|
+
db_class.define_method("begin_transaction", method!(Database::begin_transaction, 0))?;
|
|
50
|
+
db_class.define_method("close", method!(Database::close, 0))?;
|
|
51
|
+
db_class.define_method("inspect", method!(Database::inspect, 0))?;
|
|
52
|
+
db_class.define_method("to_s", method!(Database::inspect, 0))?;
|
|
53
|
+
|
|
54
|
+
// Transaction
|
|
55
|
+
let tx_class = module.define_class("Transaction", ruby.class_object())?;
|
|
56
|
+
tx_class.define_method("execute", method!(Transaction::execute, -1))?;
|
|
57
|
+
tx_class.define_method("query", method!(Transaction::query, -1))?;
|
|
58
|
+
tx_class.define_method("query_one", method!(Transaction::query_one, -1))?;
|
|
59
|
+
tx_class.define_method("query_raw", method!(Transaction::query_raw, -1))?;
|
|
60
|
+
tx_class.define_method("execute_batch", method!(Transaction::execute_batch, 2))?;
|
|
61
|
+
tx_class.define_method(
|
|
62
|
+
"execute_prepared",
|
|
63
|
+
method!(Transaction::execute_prepared, -1),
|
|
64
|
+
)?;
|
|
65
|
+
tx_class.define_method("query_prepared", method!(Transaction::query_prepared, -1))?;
|
|
66
|
+
tx_class.define_method(
|
|
67
|
+
"query_one_prepared",
|
|
68
|
+
method!(Transaction::query_one_prepared, -1),
|
|
69
|
+
)?;
|
|
70
|
+
tx_class.define_method(
|
|
71
|
+
"query_raw_prepared",
|
|
72
|
+
method!(Transaction::query_raw_prepared, -1),
|
|
73
|
+
)?;
|
|
74
|
+
tx_class.define_method("commit", method!(Transaction::commit, 0))?;
|
|
75
|
+
tx_class.define_method("rollback", method!(Transaction::rollback, 0))?;
|
|
76
|
+
tx_class.define_method("inspect", method!(Transaction::inspect, 0))?;
|
|
77
|
+
tx_class.define_method("to_s", method!(Transaction::inspect, 0))?;
|
|
78
|
+
|
|
79
|
+
// PreparedStatement
|
|
80
|
+
let stmt_class = module.define_class("PreparedStatement", ruby.class_object())?;
|
|
81
|
+
stmt_class.define_method("execute", method!(PreparedStatement::execute, -1))?;
|
|
82
|
+
stmt_class.define_method("query", method!(PreparedStatement::query, -1))?;
|
|
83
|
+
stmt_class.define_method("query_one", method!(PreparedStatement::query_one, -1))?;
|
|
84
|
+
stmt_class.define_method("query_raw", method!(PreparedStatement::query_raw, -1))?;
|
|
85
|
+
stmt_class.define_method(
|
|
86
|
+
"execute_batch",
|
|
87
|
+
method!(PreparedStatement::execute_batch, 1),
|
|
88
|
+
)?;
|
|
89
|
+
stmt_class.define_method("sql", method!(PreparedStatement::sql, 0))?;
|
|
90
|
+
stmt_class.define_method("inspect", method!(PreparedStatement::inspect, 0))?;
|
|
91
|
+
stmt_class.define_method("to_s", method!(PreparedStatement::inspect, 0))?;
|
|
92
|
+
|
|
93
|
+
// Vector
|
|
94
|
+
let vec_class = module.define_class("Vector", ruby.class_object())?;
|
|
95
|
+
vec_class.define_singleton_method("new", function!(Vector::new, 1))?;
|
|
96
|
+
vec_class.define_method("to_a", method!(Vector::to_a, 0))?;
|
|
97
|
+
vec_class.define_method("length", method!(Vector::length, 0))?;
|
|
98
|
+
vec_class.define_method("size", method!(Vector::length, 0))?;
|
|
99
|
+
vec_class.define_method("inspect", method!(Vector::inspect, 0))?;
|
|
100
|
+
vec_class.define_method("to_s", method!(Vector::inspect, 0))?;
|
|
101
|
+
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|