mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-22 10:44:19 +01:00
fix exec to run over multiple statements in the string
This commit is contained in:
@@ -167,16 +167,30 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a SQL statement.
|
||||
* Executes the given SQL string
|
||||
* Unlike prepared statements, this can execute strings that contain multiple SQL statements
|
||||
*
|
||||
* @param {string} sql - The SQL statement string to execute.
|
||||
* @param {string} sql - The string containing SQL statements to execute
|
||||
*/
|
||||
exec(sql) {
|
||||
let stmt = this.prepare(sql);
|
||||
const exec = this.db.executor(sql);
|
||||
try {
|
||||
stmt.run();
|
||||
while (true) {
|
||||
const stepResult = exec.stepSync();
|
||||
if (stepResult === STEP_IO) {
|
||||
this.db.ioLoopSync();
|
||||
continue;
|
||||
}
|
||||
if (stepResult === STEP_DONE) {
|
||||
break;
|
||||
}
|
||||
if (stepResult === STEP_ROW) {
|
||||
// For exec(), we don't need the row data, just continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stmt.close();
|
||||
exec.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,16 +165,32 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a SQL statement.
|
||||
* Executes the given SQL string
|
||||
* Unlike prepared statements, this can execute strings that contain multiple SQL statements
|
||||
*
|
||||
* @param {string} sql - The SQL statement string to execute.
|
||||
* @param {string} sql - The string containing SQL statements to execute
|
||||
*/
|
||||
async exec(sql) {
|
||||
const stmt = this.prepare(sql);
|
||||
await this.execLock.acquire();
|
||||
const exec = this.db.executor(sql);
|
||||
try {
|
||||
await stmt.run();
|
||||
while (true) {
|
||||
const stepResult = exec.stepSync();
|
||||
if (stepResult === STEP_IO) {
|
||||
await this.db.ioLoopAsync();
|
||||
continue;
|
||||
}
|
||||
if (stepResult === STEP_DONE) {
|
||||
break;
|
||||
}
|
||||
if (stepResult === STEP_ROW) {
|
||||
// For exec(), we don't need the row data, just continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await stmt.close();
|
||||
exec.reset();
|
||||
this.execLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface NativeDatabase {
|
||||
ioLoopAsync(): Promise<void>;
|
||||
|
||||
prepare(sql: string): NativeStatement;
|
||||
executor(sql: string): NativeExecutor;
|
||||
|
||||
defaultSafeIntegers(toggle: boolean);
|
||||
totalChanges(): number;
|
||||
@@ -38,6 +39,10 @@ export interface TableColumn {
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface NativeExecutor {
|
||||
stepSync(): number;
|
||||
reset();
|
||||
}
|
||||
export interface NativeStatement {
|
||||
stepAsync(): Promise<number>;
|
||||
stepSync(): number;
|
||||
|
||||
@@ -11,6 +11,14 @@ test('in-memory db', () => {
|
||||
expect(rows).toEqual([{ x: 1 }, { x: 3 }]);
|
||||
})
|
||||
|
||||
test('exec multiple statements', async () => {
|
||||
const db = new Database(":memory:");
|
||||
db.exec("CREATE TABLE t(x); INSERT INTO t VALUES (1); INSERT INTO t VALUES (2)");
|
||||
const stmt = db.prepare("SELECT * FROM t");
|
||||
const rows = stmt.all();
|
||||
expect(rows).toEqual([{ x: 1 }, { x: 2 }]);
|
||||
})
|
||||
|
||||
test('readonly-db', () => {
|
||||
const path = `test-${(Math.random() * 10000) | 0}.db`;
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
export declare class BatchExecutor {
|
||||
stepSync(): number
|
||||
reset(): void
|
||||
}
|
||||
|
||||
/** A database connection. */
|
||||
export declare class Database {
|
||||
/**
|
||||
@@ -39,6 +44,7 @@ export declare class Database {
|
||||
* A `Statement` instance.
|
||||
*/
|
||||
prepare(sql: string): Statement
|
||||
executor(sql: string): BatchExecutor
|
||||
/**
|
||||
* Returns the rowid of the last row inserted.
|
||||
*
|
||||
|
||||
@@ -508,6 +508,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { Database, Statement } = nativeBinding
|
||||
const { BatchExecutor, Database, Statement } = nativeBinding
|
||||
export { BatchExecutor }
|
||||
export { Database }
|
||||
export { Statement }
|
||||
|
||||
@@ -31,6 +31,14 @@ test('in-memory-db-async', async () => {
|
||||
expect(rows).toEqual([{ x: 1 }, { x: 3 }]);
|
||||
})
|
||||
|
||||
test('exec multiple statements', async () => {
|
||||
const db = await connect(":memory:");
|
||||
await db.exec("CREATE TABLE t(x); INSERT INTO t VALUES (1); INSERT INTO t VALUES (2)");
|
||||
const stmt = db.prepare("SELECT * FROM t");
|
||||
const rows = await stmt.all();
|
||||
expect(rows).toEqual([{ x: 1 }, { x: 2 }]);
|
||||
})
|
||||
|
||||
test('readonly-db', async () => {
|
||||
const path = `test-${(Math.random() * 10000) | 0}.db`;
|
||||
try {
|
||||
|
||||
@@ -121,12 +121,8 @@ pub struct DatabaseOpts {
|
||||
pub tracing: Option<String>,
|
||||
}
|
||||
|
||||
fn step_sync(stmt: &Arc<RefCell<Option<turso_core::Statement>>>) -> napi::Result<u32> {
|
||||
let mut stmt_ref = stmt.borrow_mut();
|
||||
let stmt = stmt_ref
|
||||
.as_mut()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
|
||||
fn step_sync(stmt: &Arc<RefCell<turso_core::Statement>>) -> napi::Result<u32> {
|
||||
let mut stmt = stmt.borrow_mut();
|
||||
match stmt.step() {
|
||||
Ok(turso_core::StepResult::Row) => Ok(STEP_ROW),
|
||||
Ok(turso_core::StepResult::IO) => Ok(STEP_IO),
|
||||
@@ -357,13 +353,23 @@ impl Database {
|
||||
.collect();
|
||||
Ok(Statement {
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
stmt: Arc::new(RefCell::new(Some(stmt))),
|
||||
stmt: Some(Arc::new(RefCell::new(stmt))),
|
||||
column_names,
|
||||
mode: RefCell::new(PresentationMode::Expanded),
|
||||
safe_integers: Cell::new(*self.inner()?.default_safe_integers.lock().unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn executor(&self, sql: String) -> napi::Result<BatchExecutor> {
|
||||
return Ok(BatchExecutor {
|
||||
conn: Some(self.conn()?.clone()),
|
||||
sql,
|
||||
position: 0,
|
||||
stmt: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the rowid of the last row inserted.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -432,10 +438,55 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct BatchExecutor {
|
||||
conn: Option<Arc<turso_core::Connection>>,
|
||||
sql: String,
|
||||
position: usize,
|
||||
stmt: Option<Arc<RefCell<turso_core::Statement>>>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl BatchExecutor {
|
||||
#[napi]
|
||||
pub fn step_sync(&mut self) -> Result<u32> {
|
||||
loop {
|
||||
if self.stmt.is_none() && self.position >= self.sql.len() {
|
||||
return Ok(STEP_DONE);
|
||||
}
|
||||
if self.stmt.is_none() {
|
||||
let conn = self.conn.as_ref().unwrap();
|
||||
match conn.consume_stmt(&self.sql[self.position..]) {
|
||||
Ok(Some((stmt, offset))) => {
|
||||
self.position += offset;
|
||||
self.stmt = Some(Arc::new(RefCell::new(stmt)));
|
||||
}
|
||||
Ok(None) => return Ok(STEP_DONE),
|
||||
Err(err) => return Err(to_generic_error("failed to consume stmt", err)),
|
||||
}
|
||||
}
|
||||
let stmt = self.stmt.as_ref().unwrap();
|
||||
match step_sync(stmt) {
|
||||
Ok(STEP_DONE) => {
|
||||
let _ = self.stmt.take();
|
||||
continue;
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
let _ = self.conn.take();
|
||||
let _ = self.stmt.take();
|
||||
}
|
||||
}
|
||||
|
||||
/// A prepared statement.
|
||||
#[napi]
|
||||
pub struct Statement {
|
||||
stmt: Arc<RefCell<Option<turso_core::Statement>>>,
|
||||
stmt: Option<Arc<RefCell<turso_core::Statement>>>,
|
||||
column_names: Vec<std::ffi::CString>,
|
||||
mode: RefCell<PresentationMode>,
|
||||
safe_integers: Cell<bool>,
|
||||
@@ -443,24 +494,21 @@ pub struct Statement {
|
||||
|
||||
#[napi]
|
||||
impl Statement {
|
||||
pub fn stmt(&self) -> napi::Result<&Arc<RefCell<turso_core::Statement>>> {
|
||||
self.stmt
|
||||
.as_ref()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
let mut stmt = self.stmt.borrow_mut();
|
||||
let stmt = stmt
|
||||
.as_mut()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
stmt.reset();
|
||||
self.stmt()?.borrow_mut().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the number of parameters in the statement.
|
||||
#[napi]
|
||||
pub fn parameter_count(&self) -> Result<u32> {
|
||||
let stmt = self.stmt.borrow();
|
||||
let stmt = stmt
|
||||
.as_ref()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
Ok(stmt.parameters_count() as u32)
|
||||
Ok(self.stmt()?.borrow().parameters_count() as u32)
|
||||
}
|
||||
|
||||
/// Returns the name of a parameter at a specific 1-based index.
|
||||
@@ -470,15 +518,11 @@ impl Statement {
|
||||
/// * `index` - The 1-based parameter index.
|
||||
#[napi]
|
||||
pub fn parameter_name(&self, index: u32) -> Result<Option<String>> {
|
||||
let stmt = self.stmt.borrow();
|
||||
let stmt = stmt
|
||||
.as_ref()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
|
||||
let non_zero_idx = NonZeroUsize::new(index as usize).ok_or_else(|| {
|
||||
create_error(Status::InvalidArg, "parameter index must be greater than 0")
|
||||
})?;
|
||||
|
||||
let stmt = self.stmt()?.borrow();
|
||||
Ok(stmt.parameters().name(non_zero_idx).map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
@@ -491,11 +535,6 @@ impl Statement {
|
||||
/// * `value` - The value to bind.
|
||||
#[napi]
|
||||
pub fn bind_at(&self, index: u32, value: Unknown) -> Result<()> {
|
||||
let mut stmt = self.stmt.borrow_mut();
|
||||
let stmt = stmt
|
||||
.as_mut()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
|
||||
let non_zero_idx = NonZeroUsize::new(index as usize).ok_or_else(|| {
|
||||
create_error(Status::InvalidArg, "parameter index must be greater than 0")
|
||||
})?;
|
||||
@@ -547,7 +586,7 @@ impl Statement {
|
||||
}
|
||||
};
|
||||
|
||||
stmt.bind_at(non_zero_idx, turso_value);
|
||||
self.stmt()?.borrow_mut().bind_at(non_zero_idx, turso_value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -555,17 +594,13 @@ impl Statement {
|
||||
/// 1 = Row available, 2 = Done, 3 = I/O needed
|
||||
#[napi]
|
||||
pub fn step_sync(&self) -> Result<u32> {
|
||||
step_sync(&self.stmt)
|
||||
step_sync(self.stmt()?)
|
||||
}
|
||||
|
||||
/// Get the current row data according to the presentation mode
|
||||
#[napi]
|
||||
pub fn row<'env>(&self, env: &'env Env) -> Result<Unknown<'env>> {
|
||||
let stmt_ref = self.stmt.borrow();
|
||||
let stmt = stmt_ref
|
||||
.as_ref()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
|
||||
let stmt = self.stmt()?.borrow();
|
||||
let row_data = stmt
|
||||
.row()
|
||||
.ok_or_else(|| create_generic_error("no row data available"))?;
|
||||
@@ -647,10 +682,7 @@ impl Statement {
|
||||
/// Get column information for the statement
|
||||
#[napi(ts_return_type = "Promise<any>")]
|
||||
pub fn columns<'env>(&self, env: &'env Env) -> Result<Array<'env>> {
|
||||
let stmt_ref = self.stmt.borrow();
|
||||
let stmt = stmt_ref
|
||||
.as_ref()
|
||||
.ok_or_else(|| create_generic_error("statement has been finalized"))?;
|
||||
let stmt = self.stmt()?.borrow();
|
||||
|
||||
let column_count = stmt.num_columns();
|
||||
let mut js_array = env.create_array(column_count as u32)?;
|
||||
@@ -682,13 +714,8 @@ impl Statement {
|
||||
|
||||
/// Finalizes the statement.
|
||||
#[napi]
|
||||
pub fn finalize(&self) -> Result<()> {
|
||||
match self.stmt.try_borrow_mut() {
|
||||
Ok(mut stmt) => {
|
||||
stmt.take();
|
||||
}
|
||||
Err(err) => tracing::error!("borrow error: {:?}", err),
|
||||
}
|
||||
pub fn finalize(&mut self) -> Result<()> {
|
||||
let _ = self.stmt.take();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
27
core/lib.rs
27
core/lib.rs
@@ -1316,6 +1316,33 @@ impl Connection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = Level::INFO)]
|
||||
pub fn consume_stmt(self: &Arc<Connection>, sql: &str) -> Result<Option<(Statement, usize)>> {
|
||||
let mut parser = Parser::new(sql.as_bytes());
|
||||
let Some(cmd) = parser.next_cmd()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let syms = self.syms.read();
|
||||
let pager = self.pager.read().clone();
|
||||
let byte_offset_end = parser.offset();
|
||||
let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end])
|
||||
.unwrap()
|
||||
.trim();
|
||||
let mode = QueryMode::new(&cmd);
|
||||
let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd;
|
||||
let program = translate::translate(
|
||||
self.schema.read().deref(),
|
||||
stmt,
|
||||
pager.clone(),
|
||||
self.clone(),
|
||||
&syms,
|
||||
mode,
|
||||
input,
|
||||
)?;
|
||||
let stmt = Statement::new(program, self.db.mv_store.clone(), pager.clone(), mode);
|
||||
Ok(Some((stmt, parser.offset())))
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs")]
|
||||
pub fn from_uri(
|
||||
uri: &str,
|
||||
|
||||
Reference in New Issue
Block a user