From acf4bdf835a96713047348f15ae2e72795c2894c Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Wed, 28 May 2025 14:13:56 +0300 Subject: [PATCH 01/13] Resolve merge conflicts --- bindings/javascript/__test__/limbo.spec.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bindings/javascript/__test__/limbo.spec.mjs b/bindings/javascript/__test__/limbo.spec.mjs index 766b1492c..9c130bacb 100644 --- a/bindings/javascript/__test__/limbo.spec.mjs +++ b/bindings/javascript/__test__/limbo.spec.mjs @@ -207,6 +207,25 @@ test("Test exec(): Should correctly load multiple statements from file", async ( t.truthy(row.name); t.true(typeof row.age === "number"); } +test("pragma query", async (t) => { + const [db] = await connect(":memory:"); + let page_size = db.pragma("page_size"); + let expectedValue = [{page_size: 4096}]; + t.deepEqual(page_size, expectedValue); +}); + +test("pragma table_list", async (t) => { + const [db] = await connect(":memory:"); + let param = "sqlite_schema"; + let actual = db.pragma(`table_info(${param})`); + let expectedValue = [ + {cid: 0, name: "type", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 1, name: "name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 2, name: "tbl_name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 3, name: "rootpage", type: "INT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 4, name: "sql", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + ]; + t.deepEqual(actual, expectedValue); }); test("Test Statement.database gets the database object", async t => { From 9f45013ec75641031a70a4c2ec47603a457f6dae Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Wed, 28 May 2025 14:17:30 +0300 Subject: [PATCH 02/13] limbo: test simple version of pragma table_list() --- bindings/javascript/__test__/limbo.spec.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bindings/javascript/__test__/limbo.spec.mjs b/bindings/javascript/__test__/limbo.spec.mjs index 9c130bacb..ea54c1187 100644 --- a/bindings/javascript/__test__/limbo.spec.mjs +++ b/bindings/javascript/__test__/limbo.spec.mjs @@ -207,6 +207,8 @@ test("Test exec(): Should correctly load multiple statements from file", async ( t.truthy(row.name); t.true(typeof row.age === "number"); } +}); + test("pragma query", async (t) => { const [db] = await connect(":memory:"); let page_size = db.pragma("page_size"); @@ -241,6 +243,14 @@ test("Test Statement.source", async t => { t.is(stmt.source, sql); }); +test("simple pragma table_list", async (t) => { + const [db] = await connect(":memory:"); + let param = "sqlite_schema"; + let actual = db.pragma(`table_info(${param})`, {simple: true}); + let expectedValue = 0; + t.deepEqual(actual, expectedValue); +}); + const connect = async (path) => { const db = new Database(path); return [db]; From 74d4726b0c946462a1eb6ee8991aef264bd81f39 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Wed, 28 May 2025 12:06:15 +0300 Subject: [PATCH 03/13] Use expect to get a better error message if accessing unavailable column --- core/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib.rs b/core/lib.rs index 49a6447d2..6a275b383 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -789,7 +789,7 @@ impl Statement { } pub fn get_column_name(&self, idx: usize) -> Cow { - let column = &self.program.result_columns[idx]; + let column = &self.program.result_columns.get(idx).expect("No column"); match column.name(&self.program.table_references) { Some(name) => Cow::Borrowed(name), None => Cow::Owned(column.expr.to_string()), From d802075ea9b59a792b34ac587111872364c50388 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Wed, 28 May 2025 12:08:14 +0300 Subject: [PATCH 04/13] Resolve merge conflict: Add columns names to result set for pragma statement output --- core/translate/pragma.rs | 12 +++++++++++- core/vdbe/builder.rs | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index d7d72fd85..54ed4176c 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -23,7 +23,7 @@ fn list_pragmas(program: &mut ProgramBuilder) { let register = program.emit_string8_new_reg(x.to_string()); program.emit_result_row(register, 1); } - + program.add_pragma_result_column("pragma_list".into()); program.epilogue(crate::translate::emitter::TransactionMode::None); } @@ -279,10 +279,12 @@ fn query_pragma( register, ); program.emit_result_row(register, 1); + program.add_pragma_result_column("cache_size".into()); } PragmaName::JournalMode => { program.emit_string8("wal".into(), register); program.emit_result_row(register, 1); + program.add_pragma_result_column("journal_mode".into()); } PragmaName::LegacyFileFormat => {} PragmaName::WalCheckpoint => { @@ -303,6 +305,7 @@ fn query_pragma( dest: register, }); program.emit_result_row(register, 1); + program.add_pragma_result_column("page_count".into()); } PragmaName::TableInfo => { let table = match value { @@ -348,6 +351,10 @@ fn query_pragma( program.emit_result_row(base_reg, 6); } } + let col_names = ["cid", "name", "type", "notnull", "dflt_value", "pk"]; + for name in col_names { + program.add_pragma_result_column(name.into()); + } } PragmaName::UserVersion => { program.emit_insn(Insn::ReadCookie { @@ -355,6 +362,7 @@ fn query_pragma( dest: register, cookie: Cookie::UserVersion, }); + program.add_pragma_result_column("user_version".into()); program.emit_result_row(register, 1); } PragmaName::SchemaVersion => { @@ -363,11 +371,13 @@ fn query_pragma( dest: register, cookie: Cookie::SchemaVersion, }); + program.add_pragma_result_column("schema_version".into()); program.emit_result_row(register, 1); } PragmaName::PageSize => { program.emit_int(database_header.lock().get_page_size().into(), register); program.emit_result_row(register, 1); + program.add_pragma_result_column("page_size".into()); } PragmaName::AutoVacuum => { let auto_vacuum_mode = pager.get_auto_vacuum_mode(); diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index c2f9bfd1f..0da96dade 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -285,6 +285,17 @@ impl ProgramBuilder { cursor } + pub fn add_pragma_result_column(&mut self, col_name: String) { + // TODO figure out a better type definition for ResultSetColumn + // or invent another way to set pragma result columns + let expr = ast::Expr::Id(ast::Id("".to_string())); + self.result_columns.push(ResultSetColumn { + expr, + alias: Some(col_name), + contains_aggregates: false, + }); + } + #[instrument(skip(self), level = Level::TRACE)] pub fn emit_insn(&mut self, insn: Insn) { let function = insn.to_function(); From 8471704e003001df947e2a5ee411b2c2417104ff Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 29 May 2025 13:02:52 +0300 Subject: [PATCH 05/13] Don't use hard-coded column names --- core/translate/pragma.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index 54ed4176c..bad117bea 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -279,12 +279,12 @@ fn query_pragma( register, ); program.emit_result_row(register, 1); - program.add_pragma_result_column("cache_size".into()); + program.add_pragma_result_column(pragma.to_string()); } PragmaName::JournalMode => { program.emit_string8("wal".into(), register); program.emit_result_row(register, 1); - program.add_pragma_result_column("journal_mode".into()); + program.add_pragma_result_column(pragma.to_string()); } PragmaName::LegacyFileFormat => {} PragmaName::WalCheckpoint => { @@ -305,7 +305,7 @@ fn query_pragma( dest: register, }); program.emit_result_row(register, 1); - program.add_pragma_result_column("page_count".into()); + program.add_pragma_result_column(pragma.to_string()); } PragmaName::TableInfo => { let table = match value { @@ -362,7 +362,7 @@ fn query_pragma( dest: register, cookie: Cookie::UserVersion, }); - program.add_pragma_result_column("user_version".into()); + program.add_pragma_result_column(pragma.to_string()); program.emit_result_row(register, 1); } PragmaName::SchemaVersion => { @@ -371,13 +371,13 @@ fn query_pragma( dest: register, cookie: Cookie::SchemaVersion, }); - program.add_pragma_result_column("schema_version".into()); + program.add_pragma_result_column(pragma.to_string()); program.emit_result_row(register, 1); } PragmaName::PageSize => { program.emit_int(database_header.lock().get_page_size().into(), register); program.emit_result_row(register, 1); - program.add_pragma_result_column("page_size".into()); + program.add_pragma_result_column(pragma.to_string()); } PragmaName::AutoVacuum => { let auto_vacuum_mode = pager.get_auto_vacuum_mode(); From dbc5e7f15ab4d26e44258b6c9b753749311b53c5 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Tue, 27 May 2025 18:35:20 +0300 Subject: [PATCH 06/13] Add simple pragma option --- bindings/javascript/src/lib.rs | 60 ++++++++++++---------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 315cd9d5e..31c39877c 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -20,6 +20,11 @@ pub struct OpenDatabaseOptions { // verbose => Callback, } +#[napi(object)] +pub struct PragmaOptions { + pub simple: bool, +} + #[napi(custom_finalize)] #[derive(Clone)] pub struct Database { @@ -82,6 +87,21 @@ impl Database { Ok(Statement::new(RefCell::new(stmt), self.clone(), sql)) } + #[napi] + pub fn transaction(&self) { + todo!() + } + + #[napi] + pub fn pragma( + &self, + env: Env, + pragma_name: String, + options: Option, + ) -> napi::Result { + todo!() + } + #[napi] pub fn backup(&self) { todo!() @@ -159,46 +179,6 @@ impl Database { self.conn.close().map_err(into_napi_error)?; Ok(()) } - - // We assume that every pragma only returns one result, which isn't - // true. - #[napi] - pub fn pragma(&self, env: Env, pragma: String, simple: bool) -> napi::Result { - let stmt = self.prepare(pragma.clone())?; - let mut stmt = stmt.inner.borrow_mut(); - let pragma_name = pragma - .split("PRAGMA") - .find(|s| !s.trim().is_empty()) - .unwrap() - .trim(); - - let mut results = env.create_empty_array()?; - - let step = stmt.step().map_err(into_napi_error)?; - match step { - limbo_core::StepResult::Row => { - let row = stmt.row().unwrap(); - let mut obj = env.create_object()?; - for value in row.get_values() { - let js_value = to_js_value(&env, value)?; - - if simple { - return Ok(js_value); - } - - obj.set_named_property(pragma_name, js_value)?; - } - - results.set_element(0, obj)?; - Ok(results.into_unknown()) - } - limbo_core::StepResult::Done => Ok(env.get_undefined()?.into_unknown()), - limbo_core::StepResult::IO => todo!(), - limbo_core::StepResult::Interrupt | limbo_core::StepResult::Busy => Err( - napi::Error::new(napi::Status::GenericFailure, format!("{:?}", step)), - ), - } - } } #[derive(Debug, Clone)] From 7c9c1f60d48a36333bad7e066ed916e1e93859d1 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 29 May 2025 12:33:39 +0300 Subject: [PATCH 07/13] Implement pragma --- bindings/javascript/src/lib.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 31c39877c..68f4fb91c 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -99,7 +99,26 @@ impl Database { pragma_name: String, options: Option, ) -> napi::Result { - todo!() + let sql = format!("PRAGMA {}", pragma_name); + let stmt = self.prepare(sql)?; + match options { + Some(PragmaOptions { simple: true, .. }) => { + let mut stmt = stmt.inner.borrow_mut(); + match stmt.step().map_err(into_napi_error)? { + limbo_core::StepResult::Row => { + let row: Vec<_> = stmt + .row() + .unwrap() + .get_values() + .map(|x| x.clone()) + .collect(); + to_js_value(&env, &row[0]) + } + _ => todo!(), + } + } + _ => stmt.run(env, None), + } } #[napi] From f9f2535246ded1b87d0db3098467c9f9a8480b95 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 29 May 2025 14:36:00 +0300 Subject: [PATCH 08/13] better-sqlite: test pragma --- .../__test__/better-sqlite3.spec.mjs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bindings/javascript/__test__/better-sqlite3.spec.mjs b/bindings/javascript/__test__/better-sqlite3.spec.mjs index 2004d28bb..c481126f4 100644 --- a/bindings/javascript/__test__/better-sqlite3.spec.mjs +++ b/bindings/javascript/__test__/better-sqlite3.spec.mjs @@ -84,6 +84,35 @@ test("Test pragma()", async (t) => { t.deepEqual(typeof db.pragma("cache_size", { simple: true }), "number"); }); +test("pragma query", async (t) => { + const [db] = await connect(":memory:"); + let page_size = db.pragma("page_size"); + let expectedValue = [{page_size: 4096}]; + t.deepEqual(page_size, expectedValue); +}); + +test("pragma table_list", async (t) => { + const [db] = await connect(":memory:"); + let param = "sqlite_schema"; + let actual = db.pragma(`table_info(${param})`); + let expectedValue = [ + {cid: 0, name: "type", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 1, name: "name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 2, name: "tbl_name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 3, name: "rootpage", type: "INT", notnull: 0, dflt_value: null, pk: 0}, + {cid: 4, name: "sql", type: "TEXT", notnull: 0, dflt_value: null, pk: 0}, + ]; + t.deepEqual(actual, expectedValue); +}); + +test("simple pragma table_list", async (t) => { + const [db] = await connect(":memory:"); + let param = "sqlite_schema"; + let actual = db.pragma(`table_info(${param})`, {simple: true}); + let expectedValue = 0; + t.deepEqual(actual, expectedValue); +}); + test("Statement shouldn't bind twice with bind()", async (t) => { const [db] = await connect(":memory:"); db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); From 2ffeb87c85af10a92f3dd2525a1e63b67f25bcc4 Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 29 May 2025 14:41:31 +0300 Subject: [PATCH 09/13] Pass options from wrapper --- bindings/javascript/wrapper.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindings/javascript/wrapper.js b/bindings/javascript/wrapper.js index 3c4e57a1d..1258a1aac 100644 --- a/bindings/javascript/wrapper.js +++ b/bindings/javascript/wrapper.js @@ -95,6 +95,8 @@ class Database { return simple ? this.db.pragma(pragma, true) : this.db.pragma(pragma, false); + ? this.db.pragma(source, { simple: true }) + : this.db.pragma(source); } backup(filename, options) { From defb2e52e882f3702c5e56032304643ad3837fbb Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 29 May 2025 14:49:21 +0300 Subject: [PATCH 10/13] process errors --- bindings/javascript/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 68f4fb91c..4b25fb9b7 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -114,7 +114,13 @@ impl Database { .collect(); to_js_value(&env, &row[0]) } - _ => todo!(), + limbo_core::StepResult::Done => Ok(env.get_undefined()?.into_unknown()), + limbo_core::StepResult::IO => todo!(), + step @ limbo_core::StepResult::Interrupt + | step @ limbo_core::StepResult::Busy => Err(napi::Error::new( + napi::Status::GenericFailure, + format!("{:?}", step), + )), } } _ => stmt.run(env, None), From a3298c3ce8c2a77823093b625a1779f6ff778f1e Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Thu, 29 May 2025 17:06:46 +0300 Subject: [PATCH 11/13] make clippy happy --- bindings/javascript/src/lib.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 4b25fb9b7..e4a7ec0e6 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -106,12 +106,7 @@ impl Database { let mut stmt = stmt.inner.borrow_mut(); match stmt.step().map_err(into_napi_error)? { limbo_core::StepResult::Row => { - let row: Vec<_> = stmt - .row() - .unwrap() - .get_values() - .map(|x| x.clone()) - .collect(); + let row: Vec<_> = stmt.row().unwrap().get_values().cloned().collect(); to_js_value(&env, &row[0]) } limbo_core::StepResult::Done => Ok(env.get_undefined()?.into_unknown()), From 8afb07803ce11f7c557d3022e1797a438a88fcbf Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Wed, 4 Jun 2025 14:36:00 +0300 Subject: [PATCH 12/13] Fix typo --- bindings/javascript/wrapper.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bindings/javascript/wrapper.js b/bindings/javascript/wrapper.js index 1258a1aac..5b5dd2342 100644 --- a/bindings/javascript/wrapper.js +++ b/bindings/javascript/wrapper.js @@ -93,8 +93,6 @@ class Database { const pragma = `PRAGMA ${source}`; return simple - ? this.db.pragma(pragma, true) - : this.db.pragma(pragma, false); ? this.db.pragma(source, { simple: true }) : this.db.pragma(source); } From 11ad9b88844bafde0603462b55775a16e9e448ce Mon Sep 17 00:00:00 2001 From: Anton Harniakou Date: Wed, 4 Jun 2025 14:37:38 +0300 Subject: [PATCH 13/13] napi.rs: Remove codegen for transaction --- bindings/javascript/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index e4a7ec0e6..6d6b15939 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -87,11 +87,6 @@ impl Database { Ok(Statement::new(RefCell::new(stmt), self.clone(), sql)) } - #[napi] - pub fn transaction(&self) { - todo!() - } - #[napi] pub fn pragma( &self,