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(); diff --git a/bindings/javascript/__test__/limbo.spec.mjs b/bindings/javascript/__test__/limbo.spec.mjs index 766b1492c..ea54c1187 100644 --- a/bindings/javascript/__test__/limbo.spec.mjs +++ b/bindings/javascript/__test__/limbo.spec.mjs @@ -209,6 +209,27 @@ test("Test exec(): Should correctly load multiple statements from file", async ( } }); +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 => { const [db] = await connect(":memory:"); let stmt = db.prepare("SELECT 1"); @@ -222,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]; diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 315cd9d5e..6d6b15939 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,36 @@ impl Database { Ok(Statement::new(RefCell::new(stmt), self.clone(), sql)) } + #[napi] + pub fn pragma( + &self, + env: Env, + pragma_name: String, + options: Option, + ) -> napi::Result { + 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().cloned().collect(); + to_js_value(&env, &row[0]) + } + 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), + } + } + #[napi] pub fn backup(&self) { todo!() @@ -159,46 +194,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)] diff --git a/bindings/javascript/wrapper.js b/bindings/javascript/wrapper.js index 3c4e57a1d..5b5dd2342 100644 --- a/bindings/javascript/wrapper.js +++ b/bindings/javascript/wrapper.js @@ -93,8 +93,8 @@ 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); } backup(filename, options) { 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()), diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index d7d72fd85..bad117bea 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(pragma.to_string()); } PragmaName::JournalMode => { program.emit_string8("wal".into(), register); program.emit_result_row(register, 1); + program.add_pragma_result_column(pragma.to_string()); } PragmaName::LegacyFileFormat => {} PragmaName::WalCheckpoint => { @@ -303,6 +305,7 @@ fn query_pragma( dest: register, }); program.emit_result_row(register, 1); + program.add_pragma_result_column(pragma.to_string()); } 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(pragma.to_string()); 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(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(pragma.to_string()); } 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();