Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions core/translate/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ pub fn translate_create_materialized_view(
program.emit_insn(Insn::ParseSchema {
db: database_id,
where_clause: Some(format!(
"name = '{escaped_view_name}' OR name = '{escaped_dbsp_table_name}' OR name = '{escaped_dbsp_index_name}'"
"(name = '{escaped_view_name}' AND type = 'view') OR \
(name = '{escaped_dbsp_table_name}' AND type = 'table') OR \
(name = '{escaped_dbsp_index_name}' AND type = 'index')"
)),
});

Expand Down Expand Up @@ -348,7 +350,7 @@ pub fn translate_create_view(
let escaped_view_name = escape_sql_string_literal(&normalized_view_name);
program.emit_insn(Insn::ParseSchema {
db: database_id,
where_clause: Some(format!("name = '{escaped_view_name}'")),
where_clause: Some(format!("name = '{escaped_view_name}' AND type = 'view'")),
});

let schema_version = resolver.with_schema(database_id, |s| s.schema_version);
Expand Down
2 changes: 2 additions & 0 deletions testing/simulator/runner/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ pub mod io;
mod mvcc_recovery;
#[cfg(test)]
mod statement_abandon;
#[cfg(test)]
mod view_trigger_parse_schema;
65 changes: 65 additions & 0 deletions testing/simulator/runner/memory/view_trigger_parse_schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::sync::Arc;

use anyhow::Result;
use turso_core::{Connection, Database, DatabaseOpts, IO, OpenFlags, StepResult};

use crate::runner::memory::io::MemorySimIO;

fn make_io(seed: u64) -> Arc<MemorySimIO> {
Arc::new(MemorySimIO::new(seed, 4096, 100, 1, 5))
}

fn open_conn(io: Arc<MemorySimIO>, path: &str) -> Result<Arc<Connection>> {
let db = Database::open_file_with_flags(
io as Arc<dyn IO>,
path,
OpenFlags::default(),
DatabaseOpts::new(),
None,
)?;
Ok(db.connect()?)
}

fn query_log_summary(conn: &Arc<Connection>, io: &MemorySimIO) -> Result<(i64, String)> {
let mut stmt = conn.prepare("SELECT count(*), group_concat(v, ',') FROM log")?;
loop {
match stmt.step()? {
StepResult::IO => io.step()?,
StepResult::Row => {
let row = stmt.row().expect("row should exist for log summary");
return Ok((
row.get::<i64>(0).expect("count column should exist"),
row.get::<String>(1)
.expect("group_concat column should exist"),
));
}
StepResult::Done => panic!("summary query ended without a row"),
other => panic!("unexpected step result: {other:?}"),
}
}
}

#[test]
fn sim_create_view_does_not_duplicate_same_named_trigger() -> Result<()> {
let seed = 301;
let io = make_io(seed);
let path = format!("sim_view_trigger_same_name_{seed}.db");

let conn = open_conn(io.clone(), &path)?;
for stmt in [
"CREATE TABLE t(x INTEGER)",
"CREATE TABLE log(v INTEGER)",
"CREATE TRIGGER dup AFTER INSERT ON t BEGIN INSERT INTO log VALUES (NEW.x); END",
"CREATE VIEW dup AS SELECT x FROM t",
"INSERT INTO t VALUES (1)",
] {
conn.execute(stmt)?;
}

assert_eq!(query_log_summary(&conn, io.as_ref())?, (1, "1".to_string()));

drop(conn);
let conn = open_conn(io.clone(), &path)?;
assert_eq!(query_log_summary(&conn, io.as_ref())?, (1, "1".to_string()));
Ok(())
}
17 changes: 17 additions & 0 deletions testing/sqltests/tests/views.sqltest
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
@database :memory:

@requires trigger "uses triggers"
@skip-if mvcc "views not supported in MVCC mode"
@cross-check-integrity
test view-create-does-not-duplicate-same-named-trigger {
CREATE TABLE t(x INTEGER);
CREATE TABLE log(v INTEGER);
CREATE TRIGGER dup AFTER INSERT ON t BEGIN
INSERT INTO log VALUES (NEW.x);
END;
CREATE VIEW dup AS SELECT x FROM t;
INSERT INTO t VALUES (1);
SELECT count(*), group_concat(v, ',') FROM log;
}
expect {
1|1
}

@cross-check-integrity
test view-basic-filtering {
CREATE TABLE products(id INTEGER, name TEXT, price INTEGER, category TEXT);
Expand Down
41 changes: 41 additions & 0 deletions tests/integration/trigger.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::common::{do_flush, ExecRows, TempDatabase};
use rusqlite::Connection as SqliteConnection;

#[turso_macros::test(mvcc)]
fn test_create_trigger(db: TempDatabase) {
Expand Down Expand Up @@ -121,6 +122,46 @@ fn test_trigger_drop_table_drops_triggers(db: TempDatabase) {
assert_eq!(results.len(), 0);
}

#[turso_macros::test]
fn test_create_view_does_not_duplicate_same_named_trigger(
tmp_db: TempDatabase,
) -> anyhow::Result<()> {
drop(tmp_db);
let limbo_db = TempDatabase::builder()
.with_db_name("view_trigger_same_name.db")
.build();
let limbo_conn = limbo_db.connect_limbo();
let sqlite_conn = SqliteConnection::open_in_memory()?;

for stmt in [
"CREATE TABLE t(x INTEGER)",
"CREATE TABLE log(v INTEGER)",
"CREATE TRIGGER dup AFTER INSERT ON t BEGIN INSERT INTO log VALUES (NEW.x); END",
"CREATE VIEW dup AS SELECT x FROM t",
] {
limbo_conn.execute(stmt)?;
sqlite_conn.execute_batch(stmt)?;
}

limbo_conn.execute("INSERT INTO t VALUES (1)")?;
sqlite_conn.execute_batch("INSERT INTO t VALUES (1)")?;

let limbo_rows: Vec<(i64, String)> =
limbo_conn.exec_rows("SELECT count(*), group_concat(v, ',') FROM log");
let sqlite_rows: Vec<(i64, String)> = sqlite_conn
.prepare("SELECT count(*), group_concat(v, ',') FROM log")?
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<rusqlite::Result<Vec<_>>>()?;
assert_eq!(limbo_rows, sqlite_rows);

drop(limbo_conn);
let reopened = limbo_db.connect_limbo();
let reopened_rows: Vec<(i64, String)> =
reopened.exec_rows("SELECT count(*), group_concat(v, ',') FROM log");
assert_eq!(reopened_rows, vec![(1, "1".to_string())]);
Ok(())
}

#[turso_macros::test(mvcc)]
fn test_trigger_new_old_references(db: TempDatabase) {
let conn = db.connect_limbo();
Expand Down
Loading