diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 233c173..6e3f783 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -2696,3 +2696,163 @@ class DiskExecutorTest { assertEquals(listOf(20L, 30L, 50L, 70L, 30L), result.rows.map { (it[1] as Value.IntegerValue).value }) } + private fun Database.seedUsersForViews() { + run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, dept TEXT)") + run( + "INSERT INTO users VALUES " + + "(1, 'ada', 36, 'eng'), (2, 'linus', 54, 'eng'), (3, 'grace', 85, 'navy'), (4, 'alan', 41, NULL)" + ) + } + + @Test + fun createViewReturnsResultAndRegistersName() { + val db = newDatabase() + db.seedUsersForViews() + val result = db.run("CREATE VIEW adults AS SELECT id, name FROM users WHERE age >= 40").single() + assertEquals(CreateViewResult("adults"), result) + assertEquals(listOf("adults"), db.catalog.viewNames) + } + + @Test + fun queryingAViewRunsItsDefiningQueryFresh() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id, name FROM users WHERE age >= 40") + val first = db.select("SELECT name FROM adults ORDER BY id ASC") + assertEquals(listOf(text("linus"), text("grace"), text("alan")), first.rows.map { it.single() }) + + db.run("INSERT INTO users VALUES (5, 'margaret', 61, 'navy')") + val second = db.select("SELECT name FROM adults ORDER BY id ASC") + assertEquals( + listOf(text("linus"), text("grace"), text("alan"), text("margaret")), + second.rows.map { it.single() } + ) + } + + @Test + fun viewCanBeUsedAsAJoinTarget() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE TABLE bonuses (dept TEXT, amount INTEGER)") + db.run("INSERT INTO bonuses VALUES ('eng', 1000), ('navy', 500)") + db.run("CREATE VIEW eng_staff AS SELECT id, name, dept FROM users WHERE dept = 'eng'") + val result = db.select( + "SELECT eng_staff.name, bonuses.amount FROM eng_staff " + + "JOIN bonuses ON eng_staff.dept = bonuses.dept ORDER BY eng_staff.id ASC" + ) + assertEquals( + listOf(listOf(text("ada"), int(1000)), listOf(text("linus"), int(1000))), + result.rows + ) + } + + @Test + fun viewOverAggregateQueryWorks() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW dept_counts AS SELECT dept, COUNT(*) AS n FROM users GROUP BY dept") + val result = db.select("SELECT dept, n FROM dept_counts WHERE n > 1 ORDER BY dept ASC") + assertEquals(listOf(listOf(text("eng"), int(2))), result.rows) + } + + @Test + fun viewDefinitionCanReferenceAnotherView() { + 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") + val result = db.select("SELECT name FROM seniors") + assertEquals(listOf(listOf(text("grace"))), result.rows) + } + + @Test + fun viewCanBeReferencedInsideAWithCteBody() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id, name, age FROM users WHERE age >= 40") + val result = db.select("WITH a AS (SELECT name FROM adults WHERE age >= 60) SELECT name FROM a") + assertEquals(listOf(listOf(text("grace"))), result.rows) + } + + @Test + fun viewCanAppearOnEitherSideOfASetOperation() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW eng AS SELECT name FROM users WHERE dept = 'eng'") + db.run("CREATE VIEW navy AS SELECT name FROM users WHERE dept = 'navy'") + val result = db.select("SELECT name FROM eng UNION SELECT name FROM navy ORDER BY name") + assertEquals(listOf(listOf(text("ada")), listOf(text("grace")), listOf(text("linus"))), result.rows) + } + + @Test + fun creatingAViewValidatesColumnsEagerly() { + val db = newDatabase() + db.seedUsersForViews() + assertFailsWith { db.run("CREATE VIEW broken AS SELECT nope FROM users") } + assertTrue(db.catalog.viewNames.isEmpty()) + } + + @Test + fun creatingAViewOverAnUnknownTableFails() { + val db = newDatabase() + assertFailsWith { db.run("CREATE VIEW v AS SELECT * FROM missing") } + } + + @Test + fun duplicateViewNameFailsWithoutOrReplace() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + assertFailsWith { db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 21") } + } + + @Test + fun createOrReplaceViewSwapsTheDefinition() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + db.run("CREATE OR REPLACE VIEW adults AS SELECT id FROM users WHERE age >= 50") + val result = db.select("SELECT id FROM adults ORDER BY id ASC") + assertEquals(listOf(listOf(int(2)), listOf(int(3))), result.rows) + } + + @Test + fun dropViewRemovesItAndItStopsBeingQueryable() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + val result = db.run("DROP VIEW adults").single() + assertEquals(DropViewResult("adults"), result) + assertFailsWith { db.run("SELECT * FROM adults") } + } + + @Test + fun droppingAnUnknownViewFails() { + val db = newDatabase() + assertFailsWith { db.run("DROP VIEW nope") } + } + + @Test + fun cannotCreateTableWithSameNameAsAnExistingView() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + assertFailsWith { db.run("CREATE TABLE adults (id INTEGER)") } + } + + @Test + fun cannotCreateViewWithSameNameAsAnExistingTable() { + val db = newDatabase() + db.seedUsersForViews() + assertFailsWith { db.run("CREATE VIEW users AS SELECT id FROM users") } + } + + @Test + fun dropTableRefusesAViewNameAndDropViewRefusesATableName() { + val db = newDatabase() + db.seedUsersForViews() + db.run("CREATE VIEW adults AS SELECT id FROM users WHERE age >= 18") + assertFailsWith { db.run("DROP TABLE adults") } + assertFailsWith { db.run("DROP VIEW users") } + } +