diff --git a/src/test/kotlin/kestrel/GeneratedColumnTest.kt b/src/test/kotlin/kestrel/GeneratedColumnTest.kt index 62a2675..01f7743 100644 --- a/src/test/kotlin/kestrel/GeneratedColumnTest.kt +++ b/src/test/kotlin/kestrel/GeneratedColumnTest.kt @@ -156,3 +156,161 @@ class GeneratedColumnTest { assertFailsWith { run("INSERT INTO t (a) VALUES (NULL)") } } + // ---- validation at CREATE TABLE ---- + + @Test + fun rejectsGeneratedExpressionReferencingANonexistentColumn() { + val error = assertFailsWith { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (missing) STORED)") + } + assertTrue(error.message!!.contains("no such column")) + } + + @Test + fun rejectsGeneratedExpressionContainingAnAggregate() { + assertFailsWith { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (COUNT(a)) STORED)") + } + } + + @Test + fun rejectsGeneratedExpressionContainingASubquery() { + run("CREATE TABLE other (x INTEGER)") + assertFailsWith { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS ((SELECT x FROM other)) STORED)") + } + } + + @Test + fun rejectsGeneratedExpressionContainingNextval() { + run("CREATE SEQUENCE seq") + assertFailsWith { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (NEXTVAL('seq')) STORED)") + } + } + + @Test + fun rejectsAGeneratedColumnReferencingAnotherGeneratedColumn() { + val error = assertFailsWith { + run( + "CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED, " + + "c INTEGER GENERATED ALWAYS AS (b + 1) STORED)" + ) + } + assertTrue(error.message!!.contains("GENERATED")) + } + + // ---- RENAME COLUMN / DROP COLUMN interaction ---- + + @Test + fun renameColumnPropagatesIntoADependentGeneratedExpression() { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)") + run("ALTER TABLE t RENAME COLUMN a TO a2") + run("INSERT INTO t (a2) VALUES (5)") + assertEquals(listOf(listOf(int(10))), select("SELECT b FROM t").rows) + } + + @Test + fun dropColumnRestrictsWhenAGeneratedColumnDependsOnIt() { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)") + val error = assertFailsWith { run("ALTER TABLE t DROP COLUMN a") } + assertTrue(error.message!!.contains("GENERATED")) + } + + @Test + fun dropColumnCascadeRemovesTheDependentGeneratedColumnToo() { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED, c TEXT)") + run("INSERT INTO t (a, c) VALUES (5, 'x')") + run("ALTER TABLE t DROP COLUMN a CASCADE") + val result = select("SELECT * FROM t") + assertEquals(listOf("c"), result.columns) + assertEquals(listOf(listOf(text("x"))), result.rows) + } + + // ---- ALTER TABLE ADD COLUMN GENERATED ALWAYS AS ---- + + @Test + fun alterTableAddColumnGeneratedBackfillsEachExistingRowFromItsOwnOtherColumns() { + run("CREATE TABLE t (a INTEGER)") + run("INSERT INTO t (a) VALUES (1), (2), (3)") + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (a * 2) STORED") + assertEquals( + listOf(listOf(int(1), int(2)), listOf(int(2), int(4)), listOf(int(3), int(6))), + select("SELECT a, b FROM t ORDER BY a ASC").rows + ) + } + + @Test + fun alterTableAddColumnGeneratedWorksOnAnEmptyTable() { + run("CREATE TABLE t (a INTEGER)") + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (a * 2) STORED") + run("INSERT INTO t (a) VALUES (5)") + assertEquals(listOf(listOf(int(10))), select("SELECT b FROM t").rows) + } + + @Test + fun afterAlterTableAddColumnGeneratedAnInsertStillRejectsAnExplicitValue() { + run("CREATE TABLE t (a INTEGER)") + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (a * 2) STORED") + val error = assertFailsWith { run("INSERT INTO t (a, b) VALUES (5, 99)") } + assertTrue(error.message!!.contains("GENERATED")) + } + + @Test + fun afterAlterTableAddColumnGeneratedUpdatingTheDependencyColumnRecomputesIt() { + run("CREATE TABLE t (a INTEGER)") + run("INSERT INTO t (a) VALUES (5)") + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (a * 2) STORED") + run("UPDATE t SET a = 9") + assertEquals(listOf(listOf(int(18))), select("SELECT b FROM t").rows) + } + + @Test + fun alterTableAddColumnGeneratedNotNullSkipsTheDefaultRequirementOnANonEmptyTable() { + run("CREATE TABLE t (a INTEGER)") + run("INSERT INTO t (a) VALUES (1), (2)") + // A plain NOT NULL column with no DEFAULT would be rejected here (see the DEFAULT-less + // ADD COLUMN rule tested elsewhere) - GENERATED has its own per-row value to backfill with + // instead, so it needs no DEFAULT at all even though the table already has rows. + run("ALTER TABLE t ADD COLUMN b INTEGER NOT NULL GENERATED ALWAYS AS (a * 2) STORED") + assertEquals(listOf(listOf(int(2)), listOf(int(4))), select("SELECT b FROM t ORDER BY a ASC").rows) + } + + @Test + fun alterTableAddColumnGeneratedNotNullRejectsARowWhoseBackfilledValueIsNull() { + run("CREATE TABLE t (a INTEGER)") + run("INSERT INTO t (a) VALUES (NULL)") + val error = assertFailsWith { + run("ALTER TABLE t ADD COLUMN b INTEGER NOT NULL GENERATED ALWAYS AS (a) STORED") + } + assertTrue(error.message!!.contains("NOT NULL")) + } + + @Test + fun alterTableAddColumnGeneratedRejectsReferencingANonexistentColumn() { + run("CREATE TABLE t (a INTEGER)") + val error = assertFailsWith { + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (missing) STORED") + } + assertTrue(error.message!!.contains("no such column")) + } + + @Test + fun alterTableAddColumnGeneratedRejectsReferencingAnAlreadyExistingGeneratedColumn() { + run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)") + val error = assertFailsWith { + run("ALTER TABLE t ADD COLUMN c INTEGER GENERATED ALWAYS AS (b + 1) STORED") + } + assertTrue(error.message!!.contains("GENERATED")) + } + + @Test + fun alterTableAddColumnGeneratedRejectsAnAggregate() { + run("CREATE TABLE t (a INTEGER)") + assertFailsWith { + run("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (COUNT(a)) STORED") + } + } + + private fun text(value: String) = Value.TextValue(value) +}