diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 6e3f783..3ae751a 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -2856,3 +2856,159 @@ class DiskExecutorTest { assertFailsWith { db.run("DROP VIEW users") } } + @Test + fun insertUpdateDeleteAgainstAViewAreRejected() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id, name FROM users WHERE age >= 18") + assertFailsWith { db.run("INSERT INTO adults VALUES (9, 'x')") } + assertFailsWith { db.run("UPDATE adults SET name = 'x'") } + assertFailsWith { db.run("DELETE FROM adults") } + } + + @Test + fun creatingAnIndexOnAViewIsRejected() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id, name FROM users WHERE age >= 18") + assertFailsWith { db.run("CREATE INDEX idx ON adults (id)") } + } + + @Test + fun mutuallyRecursiveViewsFailInsteadOfOverflowingTheStack() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW a AS SELECT id FROM users") + db.run("CREATE VIEW b AS SELECT id FROM a") + db.run("CREATE OR REPLACE VIEW a AS SELECT id FROM b") + assertFailsWith { db.run("SELECT * FROM a") } + } + + @Test + fun selfReferencingViewFailsCleanly() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW a AS SELECT id FROM users") + db.run("CREATE OR REPLACE VIEW a AS SELECT id FROM a") + assertFailsWith { db.run("SELECT * FROM a") } + } + + @Test + fun dropTableWithADependentViewFailsByDefaultAndAlsoWithExplicitRestrict() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + assertFailsWith { db.run("DROP TABLE users") } + assertFailsWith { db.run("DROP TABLE users RESTRICT") } + assertEquals(listOf("users"), db.catalog.tableNames) + assertEquals(listOf("adults"), db.catalog.viewNames) + } + + @Test + fun dropViewWithADependentViewFailsByDefault() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + db.run("CREATE VIEW seniorAdults AS SELECT id FROM adults WHERE id > 0") + assertFailsWith { db.run("DROP VIEW adults") } + assertEquals(listOf("adults", "seniorAdults"), db.catalog.viewNames) + } + + @Test + fun dropViewCascadeDropsATransitiveChainOfDependentViews() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW a AS SELECT id FROM users WHERE age >= 18") + db.run("CREATE VIEW b AS SELECT id FROM a WHERE id > 0") + db.run("CREATE VIEW c AS SELECT id FROM b WHERE id > 0") + db.run("DROP VIEW a CASCADE") + assertTrue(db.catalog.viewNames.isEmpty()) + } + + @Test + fun dropTableWithADiamondOfDependentViewsIsBlockedByEveryDirectDependent() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW b AS SELECT id FROM users WHERE age >= 18") + db.run("CREATE VIEW c AS SELECT id FROM users WHERE age >= 21") + assertFailsWith { db.run("DROP TABLE users") } + assertEquals(listOf("users"), db.catalog.tableNames) + assertEquals(listOf("b", "c"), db.catalog.viewNames) + } + + @Test + fun dropTableCascadeDropsTheTableAndItsDependentView() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + db.run("DROP TABLE users CASCADE") + assertTrue(db.catalog.tableNames.isEmpty()) + assertTrue(db.catalog.viewNames.isEmpty()) + } + + @Test + fun dropTableCascadeHandlesADiamondOfDependentViewsWithoutFailing() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW b AS SELECT id FROM users WHERE age >= 18") + db.run("CREATE VIEW c AS SELECT id FROM users WHERE age >= 21") + db.run("CREATE VIEW d AS SELECT b.id FROM b JOIN c ON b.id = c.id") + db.run("DROP TABLE users CASCADE") + assertTrue(db.catalog.tableNames.isEmpty()) + assertTrue(db.catalog.viewNames.isEmpty()) + } + + @Test + fun dropTableCascadeDroppedViewsSurviveClosingAndReopeningTheCatalog() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + db.run("DROP TABLE users CASCADE") + db.reopen() + assertTrue(db.catalog.tableNames.isEmpty()) + assertTrue(db.catalog.viewNames.isEmpty()) + } + + @Test + fun columnTypeOfAViewIsInferredFromActualData() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW names AS SELECT name FROM users") + val result = db.select("SELECT name FROM names WHERE name = 'ada'") + assertEquals(listOf(listOf(text("ada"))), result.rows) + } + + @Test + fun viewDefinitionSurvivesClosingAndReopeningTheCatalog() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id, name FROM users WHERE age >= 40") + db.reopen() + assertEquals(listOf("adults"), db.catalog.viewNames) + val result = db.select("SELECT name FROM adults ORDER BY id ASC") + assertEquals(listOf(text("linus"), text("grace"), text("alan")), result.rows.map { it.single() }) + + db.run("INSERT INTO users VALUES (5, 'margaret', 61, 'navy')") + db.reopen() + val afterInsertAndReopen = db.select("SELECT name FROM adults ORDER BY id ASC") + assertEquals( + listOf(text("linus"), text("grace"), text("alan"), text("margaret")), + afterInsertAndReopen.rows.map { it.single() } + ) + } + + @Test + fun viewOverViewAndDropViewSurviveReopen() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id, name, age FROM users WHERE age >= 40") + db.run("CREATE VIEW seniors AS SELECT id, name FROM adults WHERE age >= 60") + db.reopen() + assertEquals(listOf(listOf(text("grace"))), db.select("SELECT name FROM seniors").rows) + + db.run("DROP VIEW seniors") + db.reopen() + assertEquals(listOf("adults"), db.catalog.viewNames) + assertFailsWith { db.run("SELECT * FROM seniors") } + } +