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.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("stoolap/stoolap")
@@ -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
+ }