diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 179e288..0c5d1f0 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -795,3 +795,165 @@ class DiskExecutorTest { assertEquals(listOf(listOf(text("ops"), int(1))), result.rows) } + @Test + fun countStarWithNoGroupByAggregatesTheWholeTable() { + val db = newDatabase() + db.seedOrders() + assertEquals(listOf(listOf(int(3))), db.select("SELECT COUNT(*) FROM orders").rows) + } + + @Test + fun countStarOverAnEmptyTableStillProducesOneRow() { + val db = newDatabase() + db.run("CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER NOT NULL)") + assertEquals(listOf(listOf(int(0))), db.select("SELECT COUNT(*) FROM orders").rows) + } + + @Test + fun groupByRollupAddsPerCustomerSubtotalsAndAGrandTotal() { + val db = newDatabase() + db.seedOrders() + val result = db.select( + "SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY ROLLUP (customer_id) " + + "ORDER BY GROUPING(customer_id), customer_id" + ) + assertEquals( + listOf( + listOf(int(1), int(150)), + listOf(int(2), int(200)), + listOf(Value.NullValue, int(350)) + ), + result.rows + ) + } + + @Test + fun havingFiltersOutGroupsThatDoNotMatch() { + val db = newDatabase() + db.seedOrders() + val result = db.select("SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id HAVING SUM(amount) > 150") + assertEquals(listOf(listOf(int(2), int(200))), result.rows) + } + + @Test + fun orderByOnAnAggregateSortsTheGroupedResult() { + val db = newDatabase() + db.seedOrders() + val result = db.select("SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id ORDER BY total DESC") + assertEquals(listOf(listOf(int(2), int(200)), listOf(int(1), int(150))), result.rows) + } + + @Test + fun aProjectedColumnNotInGroupByOrAnAggregateIsRejected() { + val db = newDatabase() + db.seedOrders() + val failure = assertFailsWith { + db.select("SELECT customer_id, amount FROM orders GROUP BY customer_id") + } + assertTrue(failure.message!!.contains("must appear in GROUP BY")) + } + + @Test + fun starProjectionCombinedWithGroupByIsRejected() { + val db = newDatabase() + db.seedOrders() + val failure = assertFailsWith { db.select("SELECT * FROM orders GROUP BY customer_id") } + assertTrue(failure.message!!.contains("cannot use *")) + } + + @Test + fun groupByAggregatesOverAJoinedResult() { + val db = newDatabase() + db.seedOrders() + val result = db.select( + "SELECT customers.name, SUM(orders.amount) FROM customers JOIN orders ON customers.id = orders.customer_id " + + "GROUP BY customers.name ORDER BY customers.name" + ) + assertEquals(listOf(listOf(text("ada"), int(150)), listOf(text("linus"), int(200))), result.rows) + } + + @Test + fun dropTableCascadeWithNoDependentsBehavesLikeAPlainDrop() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY)") + db.run("DROP TABLE users CASCADE") + assertTrue(db.catalog.tableNames.isEmpty()) + } + + @Test + fun createIndexBuildsANonUniqueSingleColumnSecondaryIndex() { + val db = newDatabase() + db.seedUsers() + val created = db.run("CREATE INDEX idx_age ON users(age)").single() + assertEquals(CreateIndexResult("idx_age"), created) + assertEquals(1, db.catalog.table("users").secondaryIndexes.size) + } + + @Test + fun createUniqueIndexBuildsAUniqueSingleColumnSecondaryIndex() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, age INTEGER)") + val created = db.run("CREATE UNIQUE INDEX idx_age ON users(age)").single() + assertEquals(CreateIndexResult("idx_age"), created) + assertTrue(db.catalog.table("users").secondaryIndexes.single().unique) + } + + @Test + fun createUniqueIndexRejectsAnInsertThatWouldDuplicateAnExistingValue() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)") + db.run("CREATE UNIQUE INDEX idx_email ON users(email)") + db.run("INSERT INTO users VALUES (1, 'ada@example.com')") + assertFailsWith { db.run("INSERT INTO users VALUES (2, 'ada@example.com')") } + assertEquals(1, db.select("SELECT * FROM users").rows.size) + } + + @Test + fun createUniqueIndexAllowsMultipleNullsSinceNullsAreNeverDuplicates() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)") + db.run("CREATE UNIQUE INDEX idx_email ON users(email)") + db.run("INSERT INTO users VALUES (1, NULL), (2, NULL)") + assertEquals(2, db.select("SELECT * FROM users").rows.size) + } + + @Test + fun createUniqueIndexOverExistingDuplicateValuesFailsAndLeavesNoTraceOfTheIndex() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)") + db.run("INSERT INTO users VALUES (1, 'dup@example.com'), (2, 'dup@example.com')") + assertFailsWith { db.run("CREATE UNIQUE INDEX idx_email ON users(email)") } + assertTrue(db.catalog.table("users").secondaryIndexes.isEmpty()) + } + + @Test + fun createUniqueIndexRejectsAnUpdateThatWouldDuplicateAnotherRowsValue() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)") + db.run("CREATE UNIQUE INDEX idx_email ON users(email)") + db.run("INSERT INTO users VALUES (1, 'ada@example.com'), (2, 'grace@example.com')") + assertFailsWith { db.run("UPDATE users SET email = 'ada@example.com' WHERE id = 2") } + assertEquals(text("grace@example.com"), db.select("SELECT email FROM users WHERE id = 2").rows.single().single()) + } + + @Test + fun createUniqueIndexDataAndConstraintSurviveClosingAndReopeningTheCatalog() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)") + db.run("CREATE UNIQUE INDEX idx_email ON users(email)") + db.run("INSERT INTO users VALUES (1, 'ada@example.com')") + db.reopen() + assertFailsWith { db.run("INSERT INTO users VALUES (2, 'ada@example.com')") } + assertTrue(db.catalog.table("users").secondaryIndexes.single().unique) + } + + @Test + fun dropIndexRemovesItFromTheCatalog() { + val db = newDatabase() + db.seedUsers() + db.run("CREATE INDEX idx_age ON users(age)") + val dropped = db.run("DROP INDEX idx_age").single() + assertEquals(DropIndexResult("idx_age"), dropped) + assertTrue(db.catalog.table("users").secondaryIndexes.isEmpty()) + } +