diff --git a/src/test/kotlin/kestrel/AlterTableTest.kt b/src/test/kotlin/kestrel/AlterTableTest.kt index d5b4142..45d93e4 100644 --- a/src/test/kotlin/kestrel/AlterTableTest.kt +++ b/src/test/kotlin/kestrel/AlterTableTest.kt @@ -470,3 +470,166 @@ class AlterTableTest { assertEquals(listOf(listOf(Value.NullValue)), select("SELECT status FROM t").rows) } + @Test + fun defaultAppliesIndependentlyToEachOmittedColumn() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'new', score INTEGER DEFAULT 0)") + run("INSERT INTO t (id) VALUES (1)") + val result = exec("INSERT INTO t (id) VALUES (2)") + assertEquals(InsertResult(1), result) + assertEquals( + listOf(listOf(text("new"), int(0)), listOf(text("new"), int(0))), + select("SELECT status, score FROM t ORDER BY id ASC").rows + ) + } + + @Test + fun createTableRejectsColumnReferenceInDefault() { + val error = assertFailsWith { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, twice INTEGER DEFAULT id)") + } + assertTrue(error.message!!.contains("DEFAULT")) + } + + @Test + fun createTableRejectsSubqueryInDefault() { + run("CREATE TABLE other (x INTEGER)") + assertFailsWith { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER DEFAULT (SELECT COUNT(*) FROM other))") + } + } + + @Test + fun createTableRejectsAggregateInDefault() { + val error = assertFailsWith { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER DEFAULT COUNT(*))") + } + assertTrue(error.message!!.contains("aggregate")) + } + + @Test + fun createTableValidatesDefaultTypeEagerly() { + assertFailsWith { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER DEFAULT 'not a number')") + } + } + + // ---- DEFAULT (UPDATE) ---- + + @Test + fun updateSetToDefaultResetsTheColumnToItsDeclaredDefault() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')") + run("INSERT INTO t VALUES (1, 'shipped')") + val result = exec("UPDATE t SET status = DEFAULT WHERE id = 1") + assertEquals(UpdateResult(1), result) + assertEquals(listOf(listOf(text("pending"))), select("SELECT status FROM t").rows) + } + + @Test + fun updateSetToDefaultFallsBackToNullWhenTheColumnHasNoDeclaredDefault() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT)") + run("INSERT INTO t VALUES (1, 'keep me')") + run("UPDATE t SET note = DEFAULT WHERE id = 1") + assertEquals(listOf(listOf(Value.NullValue)), select("SELECT note FROM t").rows) + } + + @Test + fun updateSetToDefaultIsEvaluatedFreshPerRowJustLikeInsert() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, score INTEGER DEFAULT 0)") + run("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)") + val result = exec("UPDATE t SET score = DEFAULT WHERE id IN (1, 2)") + assertEquals(UpdateResult(2), result) + assertEquals( + listOf(listOf(int(1), int(0)), listOf(int(2), int(0)), listOf(int(3), int(30))), + select("SELECT id, score FROM t ORDER BY id ASC").rows + ) + } + + @Test + fun updateCanMixADefaultAssignmentWithARegularAssignmentInTheSameStatement() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, status TEXT DEFAULT 'pending')") + run("INSERT INTO t VALUES (1, 'ada', 'shipped')") + run("UPDATE t SET name = 'grace', status = DEFAULT WHERE id = 1") + assertEquals(listOf(listOf(text("grace"), text("pending"))), select("SELECT name, status FROM t").rows) + } + + @Test + fun updateSetToDefaultFailsNotNullValidationWhenThereIsNoDefaultToFallBackOn() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT NOT NULL)") + run("INSERT INTO t VALUES (1, 'shipped')") + val error = assertFailsWith { + run("UPDATE t SET status = DEFAULT WHERE id = 1") + } + assertTrue(error.message!!.contains("status"), error.message!!) + } + + @Test + fun updateSetToDefaultRoundTripsThroughSqlWriter() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')") + run("INSERT INTO t VALUES (1, 'shipped')") + val statement = Parser(Lexer("UPDATE t SET status = DEFAULT WHERE id = 1").tokenize()).parseProgram().single() + val rendered = SqlWriter.write(statement) + assertTrue(rendered.contains("status = DEFAULT"), rendered) + run(rendered) + assertEquals(listOf(listOf(text("pending"))), select("SELECT status FROM t").rows) + } + + // ---- DEFAULT (INSERT VALUES) ---- + + @Test + fun insertValuesWithLiteralDefaultUsesTheColumnsDeclaredDefault() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')") + val result = exec("INSERT INTO t VALUES (1, DEFAULT)") + assertEquals(InsertResult(1), result) + assertEquals(listOf(listOf(text("pending"))), select("SELECT status FROM t").rows) + } + + @Test + fun insertValuesWithLiteralDefaultFallsBackToNullWhenTheColumnHasNoDeclaredDefault() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT)") + run("INSERT INTO t VALUES (1, DEFAULT)") + assertEquals(listOf(listOf(Value.NullValue)), select("SELECT note FROM t").rows) + } + + @Test + fun insertValuesLiteralDefaultIsDistinctFromAnExplicitNullLiteral() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')") + run("INSERT INTO t VALUES (1, NULL)") + assertEquals(listOf(listOf(Value.NullValue)), select("SELECT status FROM t").rows) + } + + @Test + fun insertValuesLiteralDefaultAppliesIndependentlyToEachRowInAMultiRowInsert() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, score INTEGER DEFAULT 0)") + run("INSERT INTO t VALUES (1, DEFAULT), (2, 20), (3, DEFAULT)") + assertEquals( + listOf(listOf(int(1), int(0)), listOf(int(2), int(20)), listOf(int(3), int(0))), + select("SELECT id, score FROM t ORDER BY id ASC").rows + ) + } + + @Test + fun insertValuesLiteralDefaultWorksWithAnExplicitColumnList() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, status TEXT DEFAULT 'pending')") + run("INSERT INTO t (id, status, name) VALUES (1, DEFAULT, 'ada')") + assertEquals(listOf(listOf(text("ada"), text("pending"))), select("SELECT name, status FROM t").rows) + } + + @Test + fun insertValuesLiteralDefaultFailsNotNullValidationWhenThereIsNoDefaultToFallBackOn() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT NOT NULL)") + val error = assertFailsWith { + run("INSERT INTO t VALUES (1, DEFAULT)") + } + assertTrue(error.message!!.contains("status"), error.message!!) + } + + @Test + fun insertValuesLiteralDefaultRoundTripsThroughSqlWriter() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')") + val statement = Parser(Lexer("INSERT INTO t VALUES (1, DEFAULT)").tokenize()).parseProgram().single() + val rendered = SqlWriter.write(statement) + assertTrue(rendered.contains("DEFAULT"), rendered) + run(rendered) + assertEquals(listOf(listOf(text("pending"))), select("SELECT status FROM t").rows) + } +