Files
kestrel/src/test/kotlin/kestrel/AlterTableTest.kt
T

1090 lines
50 KiB
Kotlin

package kestrel
import kestrel.catalog.Catalog
import kestrel.engine.AlterTableResult
import kestrel.engine.ExplainResult
import kestrel.engine.Executor
import kestrel.engine.InsertResult
import kestrel.engine.SelectResult
import kestrel.engine.StatementResult
import kestrel.engine.UpdateResult
import kestrel.persistence.SqlWriter
import kestrel.sql.lexer.Lexer
import kestrel.sql.parser.Parser
import kestrel.storage.Value
import java.math.BigDecimal
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
/**
* `ALTER TABLE ... ADD/DROP/RENAME COLUMN` and `DEFAULT` - the engine previously had no way to
* change a table's shape after `CREATE TABLE` at all, and no way to fill a column omitted from an
* INSERT column list with anything but NULL.
*/
class AlterTableTest {
private val catalog = Catalog()
private val executor = Executor(catalog)
private fun run(sql: String): List<StatementResult> =
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
private fun exec(sql: String): StatementResult = run(sql).single()
private fun select(sql: String): SelectResult = exec(sql) as SelectResult
private fun explain(sql: String): List<String> = (exec(sql) as ExplainResult).plan
private fun int(value: Long) = Value.IntegerValue(value)
private fun text(value: String) = Value.TextValue(value)
private fun real(value: Double) = Value.RealValue(value)
private fun decimal(value: String) = Value.DecimalValue(BigDecimal(value))
// ---- ADD COLUMN ----
@Test
fun addColumnAppendsNullToExistingRowsWhenNoDefaultIsGiven() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO t VALUES (1, 'ada')")
val result = exec("ALTER TABLE t ADD COLUMN age INTEGER")
assertEquals(AlterTableResult("t"), result)
assertEquals(listOf(listOf(int(1), text("ada"), Value.NullValue)), select("SELECT * FROM t").rows)
}
@Test
fun addColumnBackfillsExistingRowsWithDefaultValue() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO t VALUES (1, 'ada'), (2, 'linus')")
run("ALTER TABLE t ADD COLUMN status TEXT DEFAULT 'active'")
assertEquals(
listOf(listOf(text("active")), listOf(text("active"))),
select("SELECT status FROM t ORDER BY id ASC").rows
)
}
@Test
fun addColumnRejectsNotNullWithoutDefaultWhenTableHasRows() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD COLUMN name TEXT NOT NULL") }
assertTrue(error.message!!.contains("NOT NULL"))
}
@Test
fun addColumnAllowsNotNullWithoutDefaultWhenTableIsEmpty() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("ALTER TABLE t ADD COLUMN name TEXT NOT NULL")
run("INSERT INTO t VALUES (1, 'ada')")
assertEquals(listOf(listOf(text("ada"))), select("SELECT name FROM t").rows)
assertFailsWith<KestrelException> { run("INSERT INTO t (id) VALUES (2)") }
}
@Test
fun addColumnWithNotNullAndDefaultBackfillsAndKeepsEnforcingGoingForward() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1)")
run("ALTER TABLE t ADD COLUMN score INTEGER NOT NULL DEFAULT 0")
assertEquals(listOf(listOf(int(0))), select("SELECT score FROM t").rows)
assertFailsWith<KestrelException> { run("INSERT INTO t (id, score) VALUES (2, NULL)") }
}
@Test
fun addColumnRejectsDuplicateColumnName() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD COLUMN name TEXT") }
assertTrue(error.message!!.contains("duplicate column"))
}
@Test
fun addColumnRequiresTableToExist() {
assertFailsWith<KestrelException> { run("ALTER TABLE missing ADD COLUMN x INTEGER") }
}
@Test
fun newColumnParticipatesInInsertUpdateWhereAndSelect() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO t VALUES (1, 'ada')")
run("ALTER TABLE t ADD COLUMN age INTEGER DEFAULT 0")
run("INSERT INTO t VALUES (2, 'linus', 54)")
run("UPDATE t SET age = 36 WHERE id = 1")
assertEquals(
listOf(listOf(text("linus"))),
select("SELECT name FROM t WHERE age > 40").rows
)
}
@Test
fun defaultExpressionCanBeAnArithmeticConstantNotJustALiteral() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1)")
run("ALTER TABLE t ADD COLUMN adjustment INTEGER DEFAULT -1 + 10")
assertEquals(listOf(listOf(int(9))), select("SELECT adjustment FROM t").rows)
}
@Test
fun addColumnRejectsPrimaryKeyReferencesUniqueAndCheckConstraints() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
assertFailsWith<ParseException> { run("ALTER TABLE t ADD COLUMN id2 INTEGER PRIMARY KEY") }
assertFailsWith<ParseException> { run("ALTER TABLE t ADD COLUMN dept_id INTEGER REFERENCES departments(id)") }
assertFailsWith<ParseException> { run("ALTER TABLE t ADD COLUMN email TEXT UNIQUE") }
assertFailsWith<ParseException> { run("ALTER TABLE t ADD COLUMN age INTEGER CHECK (age > 0)") }
}
// ---- DROP COLUMN ----
@Test
fun dropColumnRemovesColumnFromSchemaAndData() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, bio TEXT)")
run("INSERT INTO t VALUES (1, 'ada', 'mathematician')")
run("ALTER TABLE t DROP COLUMN bio")
val result = select("SELECT * FROM t")
assertEquals(listOf("id", "name"), result.columns)
assertEquals(listOf(listOf(int(1), text("ada"))), result.rows)
}
@Test
fun dropColumnRejectsWhenItIsTheOnlyColumn() {
run("CREATE TABLE t (id INTEGER)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP COLUMN id") }
assertTrue(error.message!!.contains("no columns"))
}
@Test
fun dropColumnRejectsPrimaryKeyColumnEvenWithCascade() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP COLUMN id CASCADE") }
assertTrue(error.message!!.contains("PRIMARY KEY"))
}
@Test
fun dropColumnRestrictsWhenReferencedByAnotherTablesForeignKey() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT UNIQUE)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT REFERENCES departments(code))")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE departments DROP COLUMN code") }
assertTrue(error.message!!.contains("FOREIGN KEY"))
}
@Test
fun dropColumnCascadeRemovesTheReferencingForeignKey() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT UNIQUE)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT REFERENCES departments(code))")
run("INSERT INTO departments VALUES (1, 'eng')")
run("ALTER TABLE departments DROP COLUMN code CASCADE")
// The FK is gone, so an employee can now reference a nonexistent department code freely.
run("INSERT INTO employees VALUES (1, 'ghost')")
assertEquals(listOf(listOf(text("ghost"))), select("SELECT dept_code FROM employees").rows)
}
@Test
fun dropColumnRestrictsWhenUsedByItsOwnForeignKey() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE employees DROP COLUMN dept_id") }
assertTrue(error.message!!.contains("FOREIGN KEY"))
}
@Test
fun dropColumnCascadeRemovesItsOwnForeignKey() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))")
run("ALTER TABLE employees DROP COLUMN dept_id CASCADE")
assertEquals(listOf("id"), select("SELECT * FROM employees").columns)
}
@Test
fun dropColumnRestrictsWhenUsedByAUniqueConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP COLUMN email") }
assertTrue(error.message!!.contains("UNIQUE"))
}
@Test
fun dropColumnCascadeRemovesTheUniqueConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
run("INSERT INTO t VALUES (1, 'a@x.com')")
run("ALTER TABLE t DROP COLUMN email CASCADE")
run("INSERT INTO t VALUES (2)")
assertEquals(2, select("SELECT * FROM t").rows.size)
}
@Test
fun dropColumnRestrictsWhenUsedByACheckConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP COLUMN age") }
assertTrue(error.message!!.contains("CHECK"))
}
@Test
fun dropColumnCascadeRemovesTheCheckConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))")
run("ALTER TABLE t DROP COLUMN age CASCADE")
run("INSERT INTO t VALUES (1)")
assertEquals(1, select("SELECT * FROM t").rows.size)
}
@Test
fun dropColumnRestrictsWhenUsedByAnIndex() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("CREATE INDEX idx_age ON t (age)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP COLUMN age") }
assertTrue(error.message!!.contains("idx_age"))
}
@Test
fun dropColumnCascadeRemovesTheIndex() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("CREATE INDEX idx_age ON t (age)")
run("ALTER TABLE t DROP COLUMN age CASCADE")
assertTrue(catalog.indexNames.isEmpty())
}
// ---- RENAME COLUMN ----
@Test
fun renameColumnRenamesSchemaAndPreservesData() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, bio TEXT)")
run("INSERT INTO t VALUES (1, 'mathematician')")
run("ALTER TABLE t RENAME COLUMN bio TO biography")
val result = select("SELECT * FROM t")
assertEquals(listOf("id", "biography"), result.columns)
assertEquals(listOf(listOf(int(1), text("mathematician"))), result.rows)
}
@Test
fun renameColumnRejectsUnknownColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
assertFailsWith<KestrelException> { run("ALTER TABLE t RENAME COLUMN missing TO whatever") }
}
@Test
fun renameColumnRejectsWhenNewNameAlreadyExists() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t RENAME COLUMN name TO id") }
assertTrue(error.message!!.contains("already exists"))
}
@Test
fun renameColumnUpdatesCompositePrimaryKeyListing() {
run("CREATE TABLE t (region TEXT, seq INTEGER, PRIMARY KEY (region, seq))")
run("INSERT INTO t VALUES ('eu', 1)")
run("ALTER TABLE t RENAME COLUMN region TO area")
assertFailsWith<KestrelException> { run("INSERT INTO t VALUES ('eu', 1)") }
run("INSERT INTO t VALUES ('eu', 2)")
assertEquals(2, select("SELECT * FROM t").rows.size)
}
@Test
fun renameColumnUpdatesItsOwnForeignKeyColumns() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))")
run("INSERT INTO departments VALUES (1)")
run("ALTER TABLE employees RENAME COLUMN dept_id TO department_id")
assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (1, 99)") }
run("INSERT INTO employees VALUES (1, 1)")
assertEquals(1, select("SELECT * FROM employees").rows.size)
}
@Test
fun renameColumnPropagatesToOtherTablesForeignKeyReferencedColumns() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id) ON DELETE CASCADE)")
run("INSERT INTO departments VALUES (1)")
run("INSERT INTO employees VALUES (1, 1)")
run("ALTER TABLE departments RENAME COLUMN id TO dept_pk")
run("DELETE FROM departments WHERE dept_pk = 1")
assertTrue(select("SELECT * FROM employees").rows.isEmpty(), "cascade must still work through the renamed referenced column")
}
@Test
fun renameColumnUpdatesAUniqueConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
run("INSERT INTO t VALUES (1, 'a@x.com')")
run("ALTER TABLE t RENAME COLUMN email TO contact_email")
assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (2, 'a@x.com')") }
}
@Test
fun renameColumnUpdatesACheckConstraintAndStillEnforcesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))")
run("ALTER TABLE t RENAME COLUMN age TO years")
assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (1, -5)") }
run("INSERT INTO t VALUES (1, 30)")
assertEquals(listOf(listOf(int(30))), select("SELECT years FROM t").rows)
}
@Test
fun renameColumnUpdatesAnIndexAndThePlannerStillUsesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("INSERT INTO t VALUES (1, 30), (2, 40)")
run("CREATE INDEX idx_age ON t (age)")
run("ALTER TABLE t RENAME COLUMN age TO years")
val plan = explain("EXPLAIN SELECT id FROM t WHERE years = 30").joinToString("\n")
assertTrue(plan.contains("Index Scan using idx_age"), plan)
assertEquals(listOf(listOf(int(1))), select("SELECT id FROM t WHERE years = 30").rows)
}
// ---- RENAME TABLE ----
@Test
fun renameTableRenamesAndPreservesData() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO t VALUES (1, 'ada')")
val result = exec("ALTER TABLE t RENAME TO people")
assertEquals(AlterTableResult("people"), result)
assertEquals(listOf(listOf(int(1), text("ada"))), select("SELECT * FROM people").rows)
assertFailsWith<KestrelException> { run("SELECT * FROM t") }
assertEquals(listOf("people"), catalog.tableNames)
}
@Test
fun renameTableRejectsWhenNewNameAlreadyExistsAsTable() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("CREATE TABLE other (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t RENAME TO other") }
assertTrue(error.message!!.contains("already exists"))
}
@Test
fun renameTableRejectsWhenNewNameAlreadyExistsAsView() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("CREATE TABLE other (id INTEGER PRIMARY KEY)")
run("CREATE VIEW v AS SELECT * FROM other")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t RENAME TO v") }
assertTrue(error.message!!.contains("already exists"))
}
@Test
fun renameTableRejectsRenamingAView() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("CREATE VIEW v AS SELECT * FROM t")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE v RENAME TO w") }
assertTrue(error.message!!.contains("cannot modify view"))
}
@Test
fun renameTablePropagatesToOtherTablesForeignKeyReference() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id) ON DELETE CASCADE)")
run("INSERT INTO departments VALUES (1)")
run("INSERT INTO employees VALUES (1, 1)")
run("ALTER TABLE departments RENAME TO divisions")
run("INSERT INTO divisions VALUES (2)")
run("INSERT INTO employees VALUES (2, 2)")
assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (3, 99)") }
run("DELETE FROM divisions WHERE id = 1")
assertEquals(listOf(listOf(int(2))), select("SELECT id FROM employees").rows)
}
@Test
fun renameTableUpdatesItsOwnSelfReferencingForeignKey() {
run(
"CREATE TABLE staff (id INTEGER PRIMARY KEY, manager_id INTEGER REFERENCES staff(id))"
)
run("INSERT INTO staff VALUES (1, NULL)")
run("ALTER TABLE staff RENAME TO personnel")
run("INSERT INTO personnel VALUES (2, 1)")
assertFailsWith<KestrelException> { run("INSERT INTO personnel VALUES (3, 99)") }
assertEquals(2, select("SELECT * FROM personnel").rows.size)
}
@Test
fun renameTableUpdatesAnIndexAndThePlannerStillUsesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("INSERT INTO t VALUES (1, 30), (2, 40)")
run("CREATE INDEX idx_age ON t (age)")
run("ALTER TABLE t RENAME TO people")
val plan = explain("EXPLAIN SELECT id FROM people WHERE age = 30").joinToString("\n")
assertTrue(plan.contains("Index Scan using idx_age on people"), plan)
assertEquals(listOf(listOf(int(1))), select("SELECT id FROM people WHERE age = 30").rows)
}
@Test
fun renameTablePropagatesIntoASimpleViewDefinition() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO t VALUES (1, 'ada')")
run("CREATE VIEW v AS SELECT * FROM t")
run("ALTER TABLE t RENAME TO people")
assertEquals(listOf(listOf(int(1), text("ada"))), select("SELECT * FROM v").rows)
}
@Test
fun renameTablePropagatesIntoAViewJoinAndASubqueryExpression() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1)")
run(
"CREATE VIEW v AS SELECT e.id FROM employees e JOIN departments d ON e.dept_id = d.id " +
"WHERE d.id IN (SELECT id FROM departments)"
)
run("ALTER TABLE departments RENAME TO divisions")
assertEquals(listOf(listOf(int(1))), select("SELECT * FROM v").rows)
}
@Test
fun renameTablePropagatesIntoADerivedTableAndAUnion() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1), (2)")
run(
"CREATE VIEW v AS SELECT * FROM (SELECT id FROM t) sub " +
"UNION SELECT id FROM t WHERE id = 2"
)
run("ALTER TABLE t RENAME TO renamed")
assertEquals(listOf(listOf(int(1)), listOf(int(2))), select("SELECT * FROM v ORDER BY id").rows)
}
@Test
fun renameTableDoesNotTouchATableSourceShadowedByACteOfTheSameName() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1)")
run("CREATE VIEW v AS WITH t AS (SELECT 99 AS id FROM t) SELECT * FROM t")
run("ALTER TABLE t RENAME TO renamed")
// The CTE named "t" shadows the base table for the rest of that WITH clause, so the view
// must still yield the CTE's own row (99), not read from the (now renamed) base table.
assertEquals(listOf(listOf(int(99))), select("SELECT * FROM v").rows)
assertEquals(listOf(listOf(int(1))), select("SELECT * FROM renamed").rows)
}
@Test
fun renameTableRenamingToItsOwnNameIsANoOp() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1)")
run("ALTER TABLE t RENAME TO t")
assertEquals(listOf(listOf(int(1))), select("SELECT * FROM t").rows)
}
// ---- DEFAULT (CREATE TABLE + INSERT) ----
@Test
fun insertUsesColumnDefaultWhenColumnOmittedFromColumnList() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')")
run("INSERT INTO t (id) VALUES (1)")
assertEquals(listOf(listOf(text("pending"))), select("SELECT status FROM t").rows)
}
@Test
fun insertDoesNotUseDefaultWhenExplicitNullIsGiven() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')")
run("INSERT INTO t (id, status) VALUES (1, NULL)")
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<KestrelException> {
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<KestrelException> {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER DEFAULT (SELECT COUNT(*) FROM other))")
}
}
@Test
fun createTableRejectsAggregateInDefault() {
val error = assertFailsWith<KestrelException> {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER DEFAULT COUNT(*))")
}
assertTrue(error.message!!.contains("aggregate"))
}
@Test
fun createTableValidatesDefaultTypeEagerly() {
assertFailsWith<KestrelException> {
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<KestrelException> {
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<KestrelException> {
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)
}
// ---- ADD CONSTRAINT / DROP CONSTRAINT ----
@Test
fun addConstraintCheckSucceedsWhenEveryExistingRowSatisfiesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("INSERT INTO t VALUES (1, 30), (2, 40)")
val result = exec("ALTER TABLE t ADD CONSTRAINT age_positive CHECK (age > 0)")
assertEquals(AlterTableResult("t"), result)
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (3, -1)") }
assertTrue(error.message!!.contains("age_positive"), error.message!!)
}
@Test
fun addConstraintCheckRejectsWhenAnExistingRowAlreadyViolatesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("INSERT INTO t VALUES (1, 30), (2, -5)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT age_positive CHECK (age > 0)") }
assertTrue(error.message!!.contains("age_positive"), error.message!!)
// Rejected, so never installed: a fresh negative row must still be free to insert.
run("INSERT INTO t VALUES (3, -1)")
assertEquals(2, select("SELECT id FROM t WHERE age < 0").rows.size)
}
@Test
fun addConstraintCheckRejectsAReferenceToAnUnknownColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT c1 CHECK (bogus > 0)") }
assertTrue(error.message!!.contains("bogus"), error.message!!)
}
@Test
fun addConstraintCheckNameRoundTripsThroughSqlWriterDumpAndReplay() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("ALTER TABLE t ADD CONSTRAINT age_positive CHECK (age > 0)")
val dumped = SqlWriter.dump(catalog)
assertTrue(dumped.contains("CONSTRAINT age_positive CHECK"), dumped)
val freshCatalog = Catalog()
val freshExecutor = Executor(freshCatalog)
for (statement in Parser(Lexer(dumped).tokenize()).parseProgram()) freshExecutor.execute(statement)
val error = assertFailsWith<KestrelException> {
freshExecutor.execute(Parser(Lexer("INSERT INTO t VALUES (5, -1)").tokenize()).parseProgram().single())
}
assertTrue(error.message!!.contains("age_positive"), error.message!!)
}
@Test
fun addConstraintUniqueSucceedsWhenNoExistingRowsAreDuplicates() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT)")
run("INSERT INTO t VALUES (1, 'a@x.com'), (2, 'b@x.com')")
exec("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)")
run("INSERT INTO t VALUES (3, 'c@x.com')")
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (4, 'c@x.com')") }
assertTrue(error.message!!.contains("uq_email"), error.message!!)
}
@Test
fun addConstraintUniqueRejectsWhenExistingRowsAreAlreadyDuplicated() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT)")
run("INSERT INTO t VALUES (1, 'a@x.com'), (2, 'a@x.com')")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)") }
assertTrue(error.message!!.contains("uq_email"), error.message!!)
}
@Test
fun addConstraintUniqueIgnoresNullValuesAmongExistingRows() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT)")
run("INSERT INTO t VALUES (1, NULL), (2, NULL)")
exec("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)")
run("INSERT INTO t VALUES (3, NULL)")
assertEquals(3, select("SELECT id FROM t").rows.size)
}
@Test
fun addConstraintUniqueRejectsAReferenceToAnUnknownColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT uq_bogus UNIQUE (bogus)") }
assertTrue(error.message!!.contains("bogus"), error.message!!)
}
@Test
fun addConstraintForeignKeySucceedsWhenEveryExistingRowHasAMatchOrIsNull() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1), (2, NULL)")
exec("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id)")
val error = assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (3, 99)") }
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun addConstraintForeignKeyRejectsWhenAnExistingRowIsAlreadyOrphaned() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1), (2, 99)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id)")
}
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun addConstraintForeignKeySelfReferenceValidatesAgainstExistingSiblingRows() {
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, manager_id INTEGER)")
run("INSERT INTO employees VALUES (1, NULL), (2, 1), (3, 99)")
assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_manager FOREIGN KEY (manager_id) REFERENCES employees (id)")
}
run("UPDATE employees SET manager_id = 1 WHERE id = 3")
exec("ALTER TABLE employees ADD CONSTRAINT fk_manager FOREIGN KEY (manager_id) REFERENCES employees (id)")
val error = assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (4, 100)") }
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun addConstraintForeignKeyRejectsWhenReferencedColumnsAreNotEnforcedUnique() {
run("CREATE TABLE departments (id INTEGER, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id)")
}
assertTrue(error.message!!.contains("PRIMARY KEY or UNIQUE"), error.message!!)
}
@Test
fun addConstraintForeignKeyRejectsAColumnCountMismatch() {
// Caught by the parser itself (parseInlineForeignKey, shared with CREATE TABLE's own
// table-level FOREIGN KEY clause) before Executor.executeAddForeignKeyConstraint's own
// redundant arity check would even run - same as CREATE TABLE's identical redundancy.
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_bad FOREIGN KEY (dept_id) REFERENCES departments (id, name)")
}
assertTrue(error.message!!.contains("column(s)"), error.message!!)
}
@Test
fun addConstraintForeignKeyOnDeleteCascadeAppliesAfterBeingAdded() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1), (2, 1)")
exec("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id) ON DELETE CASCADE")
run("DELETE FROM departments WHERE id = 1")
assertEquals(emptyList(), select("SELECT id FROM employees").rows)
}
@Test
fun addConstraintRejectsANameAlreadyUsedByADifferentKindOfConstraintOnTheSameTable() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, email TEXT)")
run("ALTER TABLE t ADD CONSTRAINT c1 CHECK (age > 0)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT c1 UNIQUE (email)") }
assertTrue(error.message!!.contains("c1"), error.message!!)
}
@Test
fun createTableAcceptsANamedTableLevelCheckConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT chk_age CHECK (age > 0))")
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (1, -1)") }
assertTrue(error.message!!.contains("chk_age"), error.message!!)
}
@Test
fun createTableAcceptsANamedTableLevelUniqueConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, CONSTRAINT uq_email UNIQUE (email))")
run("INSERT INTO t VALUES (1, 'a@x.com')")
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (2, 'a@x.com')") }
assertTrue(error.message!!.contains("uq_email"), error.message!!)
}
@Test
fun createTableAcceptsANamedTableLevelForeignKeyConstraint() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER, " +
"CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id))"
)
run("INSERT INTO departments VALUES (1)")
val error = assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (1, 99)") }
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun dropConstraintRemovesANamedCheckConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT chk_age CHECK (age > 0))")
exec("ALTER TABLE t DROP CONSTRAINT chk_age")
run("INSERT INTO t VALUES (1, -5)")
assertEquals(listOf(listOf(int(1))), select("SELECT id FROM t").rows)
}
@Test
fun dropConstraintRemovesANamedUniqueConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, CONSTRAINT uq_email UNIQUE (email))")
run("INSERT INTO t VALUES (1, 'a@x.com')")
exec("ALTER TABLE t DROP CONSTRAINT uq_email")
run("INSERT INTO t VALUES (2, 'a@x.com')")
assertEquals(2, select("SELECT id FROM t").rows.size)
}
@Test
fun dropConstraintRemovesANamedForeignKeyConstraint() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER, " +
"CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id))"
)
exec("ALTER TABLE employees DROP CONSTRAINT fk_dept")
run("INSERT INTO employees VALUES (1, 99)")
assertEquals(listOf(listOf(int(1))), select("SELECT id FROM employees").rows)
}
@Test
fun dropConstraintFailsForAnUnknownName() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP CONSTRAINT nope") }
assertTrue(error.message!!.contains("no such constraint"), error.message!!)
}
@Test
fun dropConstraintRestrictsWhenAnotherTablesForeignKeyDependsOnTheUniqueConstraint() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT, CONSTRAINT uq_code UNIQUE (code))")
run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT, " +
"CONSTRAINT fk_dept_code FOREIGN KEY (dept_code) REFERENCES departments (code))"
)
val error = assertFailsWith<KestrelException> { run("ALTER TABLE departments DROP CONSTRAINT uq_code") }
assertTrue(error.message!!.contains("employees"), error.message!!)
}
@Test
fun dropConstraintCascadeRemovesTheDependentForeignKeyOnAnotherTable() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT, CONSTRAINT uq_code UNIQUE (code))")
run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT, " +
"CONSTRAINT fk_dept_code FOREIGN KEY (dept_code) REFERENCES departments (code))"
)
exec("ALTER TABLE departments DROP CONSTRAINT uq_code CASCADE")
run("INSERT INTO employees VALUES (1, 'nonexistent')")
assertEquals(listOf(listOf(int(1))), select("SELECT id FROM employees").rows)
}
@Test
fun dropConstraintRestrictsASelfReferencingForeignKeyDependentOnItsOwnUniqueConstraint() {
run(
"CREATE TABLE t (id INTEGER PRIMARY KEY, code TEXT, parent_code TEXT, " +
"CONSTRAINT uq_code UNIQUE (code), CONSTRAINT fk_parent FOREIGN KEY (parent_code) REFERENCES t (code))"
)
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t DROP CONSTRAINT uq_code") }
assertTrue(error.message!!.contains("FOREIGN KEY"), error.message!!)
}
@Test
fun dropConstraintCascadeRemovesASelfReferencingForeignKeyThatDependedOnTheDroppedUniqueConstraint() {
run(
"CREATE TABLE t (id INTEGER PRIMARY KEY, code TEXT, parent_code TEXT, " +
"CONSTRAINT uq_code UNIQUE (code), CONSTRAINT fk_parent FOREIGN KEY (parent_code) REFERENCES t (code))"
)
exec("ALTER TABLE t DROP CONSTRAINT uq_code CASCADE")
run("INSERT INTO t VALUES (1, 'a', 'nonexistent')")
assertEquals(listOf(listOf(int(1))), select("SELECT id FROM t").rows)
}
@Test
fun dropConstraintAllowsDroppingAUniqueConstraintWhenAnotherRemainingGroupStillCoversTheSameColumns() {
run(
"CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT, " +
"CONSTRAINT uq_code_a UNIQUE (code), CONSTRAINT uq_code_b UNIQUE (code))"
)
run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT, " +
"CONSTRAINT fk_dept_code FOREIGN KEY (dept_code) REFERENCES departments (code))"
)
exec("ALTER TABLE departments DROP CONSTRAINT uq_code_a")
val error = assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (1, 'nonexistent')") }
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
// ---- ALTER COLUMN TYPE ----
@Test
fun changeColumnTypeWideningIntegerToRealCoercesExistingRowsWithNoExplicitUsing() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, score INTEGER)")
run("INSERT INTO t VALUES (1, 10), (2, 20)")
val result = exec("ALTER TABLE t ALTER COLUMN score TYPE REAL")
assertEquals(AlterTableResult("t"), result)
assertEquals(listOf(listOf(real(10.0)), listOf(real(20.0))), select("SELECT score FROM t ORDER BY id ASC").rows)
}
@Test
fun changeColumnTypeWideningIntegerToDecimalCoercesExistingRows() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER)")
run("INSERT INTO t VALUES (1, 42)")
exec("ALTER TABLE t ALTER COLUMN amount TYPE DECIMAL(10, 2)")
assertEquals(listOf(listOf(decimal("42.00"))), select("SELECT amount FROM t").rows)
}
@Test
fun changeColumnTypeWithUsingConvertsTextToIntegerViaCast() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, code TEXT)")
run("INSERT INTO t VALUES (1, '100'), (2, '200')")
exec("ALTER TABLE t ALTER COLUMN code TYPE INTEGER USING CAST(code AS INTEGER)")
assertEquals(listOf(listOf(int(100)), listOf(int(200))), select("SELECT code FROM t ORDER BY id ASC").rows)
}
@Test
fun changeColumnTypeWithUsingCanReferenceTheRowsOtherColumns() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, price INTEGER, quantity INTEGER)")
run("INSERT INTO t VALUES (1, 10, 3)")
exec("ALTER TABLE t ALTER COLUMN price TYPE INTEGER USING price * quantity")
assertEquals(listOf(listOf(int(30))), select("SELECT price FROM t").rows)
}
@Test
fun changeColumnTypeRejectsImplicitCoercionWhenTypesAreIncompatibleAndNoUsingIsGiven() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, code TEXT)")
run("INSERT INTO t VALUES (1, 'abc')")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN code TYPE INTEGER") }
assertTrue(error.message!!.contains("type mismatch"), error.message!!)
}
@Test
fun changeColumnTypeRejectsUnknownColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN missing TYPE TEXT") }
assertTrue(error.message!!.contains("no such column"), error.message!!)
}
@Test
fun changeColumnTypeRejectsGeneratedColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, price INTEGER, doubled INTEGER GENERATED ALWAYS AS (price * 2) STORED)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN doubled TYPE REAL") }
assertTrue(error.message!!.contains("GENERATED"), error.message!!)
}
@Test
fun changeColumnTypeRejectsPrimaryKeyColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN id TYPE TEXT") }
assertTrue(error.message!!.contains("PRIMARY KEY"), error.message!!)
}
@Test
fun changeColumnTypeRejectsColumnUsedByItsOwnForeignKey() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER, FOREIGN KEY (dept_id) REFERENCES departments (id))")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE employees ALTER COLUMN dept_id TYPE TEXT") }
assertTrue(error.message!!.contains("FOREIGN KEY"), error.message!!)
}
@Test
fun changeColumnTypeRejectsColumnReferencedByAnotherTablesForeignKey() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT, CONSTRAINT uq_code UNIQUE (code))")
run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT, " +
"FOREIGN KEY (dept_code) REFERENCES departments (code))"
)
val error = assertFailsWith<KestrelException> { run("ALTER TABLE departments ALTER COLUMN code TYPE INTEGER") }
assertTrue(error.message!!.contains("employees"), error.message!!)
}
@Test
fun changeColumnTypeRejectsColumnUsedByAUniqueConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT, CONSTRAINT uq_email UNIQUE (email))")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN email TYPE TEXT") }
assertTrue(error.message!!.contains("UNIQUE"), error.message!!)
}
@Test
fun changeColumnTypeRejectsColumnWithASecondaryIndex() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE INDEX idx_name ON t (name)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN name TYPE TEXT") }
assertTrue(error.message!!.contains("idx_name"), error.message!!)
}
@Test
fun changeColumnTypeRejectsWhenUsingProducesNullForANotNullColumnAndLeavesTheTableUnchanged() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, code TEXT NOT NULL)")
run("INSERT INTO t VALUES (1, '100'), (2, '')")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE t ALTER COLUMN code TYPE INTEGER USING CASE WHEN code = '' THEN NULL ELSE CAST(code AS INTEGER) END")
}
assertTrue(error.message!!.contains("NOT NULL"), error.message!!)
assertEquals(listOf(listOf(text("100")), listOf(text(""))), select("SELECT code FROM t ORDER BY id ASC").rows)
}
@Test
fun changeColumnTypeRevalidatesACheckConstraintAgainstTheNewValuesAndLeavesTheTableUnchangedOnFailure() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER, CHECK (amount <= 100))")
run("INSERT INTO t VALUES (1, 10), (2, 60)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE t ALTER COLUMN amount TYPE INTEGER USING amount * 2")
}
assertTrue(error.message!!.contains("CHECK constraint"), error.message!!)
assertEquals(
listOf(listOf(int(1), int(10)), listOf(int(2), int(60))),
select("SELECT id, amount FROM t ORDER BY id ASC").rows
)
}
@Test
fun changeColumnTypeSucceedsAndAppliesUsingToEveryRowWhenAllSatisfyAnExistingCheckConstraint() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER, CHECK (amount <= 100))")
run("INSERT INTO t VALUES (1, 10), (2, 20)")
exec("ALTER TABLE t ALTER COLUMN amount TYPE INTEGER USING amount * 2")
assertEquals(listOf(listOf(int(20)), listOf(int(40))), select("SELECT amount FROM t ORDER BY id ASC").rows)
}
@Test
fun changeColumnTypeRejectsAnIncompatibleDefaultExpressionUpFront() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'pending')")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ALTER COLUMN status TYPE INTEGER USING 0") }
assertTrue(error.message!!.contains("DEFAULT"), error.message!!)
}
@Test
fun changeColumnTypePreservesNotNullAndStillEnforcesItGoingForward() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER NOT NULL)")
run("INSERT INTO t VALUES (1, 10)")
exec("ALTER TABLE t ALTER COLUMN amount TYPE REAL")
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (2, NULL)") }
assertTrue(error.message!!.contains("NOT NULL"), error.message!!)
}
@Test
fun changeColumnTypeAcceptsAnOptionalColumnKeyword() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER)")
run("INSERT INTO t VALUES (1, 5)")
exec("ALTER TABLE t ALTER amount TYPE REAL")
assertEquals(listOf(listOf(real(5.0))), select("SELECT amount FROM t").rows)
}
@Test
fun changeColumnTypeDumpReflectsTheNewDeclaredType() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER)")
run("INSERT INTO t VALUES (1, 5)")
exec("ALTER TABLE t ALTER COLUMN amount TYPE DECIMAL(10, 2)")
val dumped = SqlWriter.dump(catalog)
assertTrue(dumped.contains("amount DECIMAL(10, 2)"), dumped)
}
@Test
fun changeColumnTypeRedoLogTextRoundTripsThroughSqlWriter() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER)")
run("INSERT INTO t VALUES (1, 5)")
val statement = Parser(Lexer("ALTER TABLE t ALTER COLUMN amount TYPE REAL USING amount").tokenize()).parseProgram().single()
val rendered = SqlWriter.write(statement)
assertTrue(rendered.contains("ALTER COLUMN amount TYPE REAL USING amount"), rendered)
run(rendered)
assertEquals(listOf(listOf(real(5.0))), select("SELECT amount FROM t").rows)
}
}