test: cover renameTablePropagatesIntoAViewsDistinctOnSubquery

This commit is contained in:
dualfroz committed 2026-09-09 19:07:52 +02:00
1 parent 118d58728f
commit 965cc2974d
1 file changed
+173
+173
View File
@@ -718,4 +718,177 @@ class PersistenceTest {
CREATE TABLE sales (dept TEXT, amount INTEGER); CREATE TABLE sales (dept TEXT, amount INTEGER);
INSERT INTO sales VALUES ('eng', 100), ('eng', 200), ('ops', 50); INSERT INTO sales VALUES ('eng', 100), ('eng', 200), ('ops', 50);
CREATE VIEW top_per_dept AS SELECT DISTINCT ON (dept) dept, amount FROM sales ORDER BY dept, amount DESC; CREATE VIEW top_per_dept AS SELECT DISTINCT ON (dept) dept, amount FROM sales ORDER BY dept, amount DESC;
.exit
""".trimIndent()
)
val reopened = runScript(dir, "SELECT dept, amount FROM top_per_dept ORDER BY dept;\n.exit\n")
assertContains(reopened, "2 rows")
assertContains(reopened, "200")
assertTrue(!reopened.contains("100"), "amount=100 lost the DISTINCT ON tie-break and must not survive")
}
/**
* Mirrors [renameTablePropagatesIntoAViewsStringAggSeparatorSubquery] for the newly added
* [kestrel.sql.ast.SelectStatement.distinctOn] field - [kestrel.catalog.RelationRename] must walk
* into it exactly like it already does for `groupBy`/`having`, or a subquery hiding inside
* `DISTINCT ON (...)` would silently keep pointing at a table name that no longer exists after
* reopen (the rewritten AST, not the original text, is what SqlWriter dumps on checkpoint).
*/
@Test
fun renameTablePropagatesIntoAViewsDistinctOnSubquery() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE items (dept TEXT, amount INTEGER);
INSERT INTO items VALUES ('eng', 100), ('eng', 200), ('ops', 50);
CREATE TABLE config (id INTEGER PRIMARY KEY, threshold INTEGER);
INSERT INTO config VALUES (1, 0);
CREATE VIEW top_per_dept AS SELECT DISTINCT ON (dept, (SELECT threshold FROM config WHERE id = 1)) dept, amount FROM items ORDER BY dept, (SELECT threshold FROM config WHERE id = 1), amount DESC;
ALTER TABLE config RENAME TO settings;
.exit
""".trimIndent()
)
val reopened = runScript(dir, "SELECT dept, amount FROM top_per_dept ORDER BY dept;\n.exit\n")
assertContains(reopened, "2 rows")
assertContains(reopened, "200")
assertContains(reopened, "50")
}
/**
* A trigger created before its table's data would refire on every reload (SqlWriter.dump
* intentionally writes CREATE TRIGGER only after every table's rows) - this proves reopening
* does NOT duplicate the audit row the trigger already produced during the first session,
* while a genuinely new INSERT after reopening still fires it normally.
*/
@Test
fun triggerCreationIsDumpedAfterDataSoReloadDoesNotRefireForAlreadyPersistedRows() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER);
CREATE TABLE audit (order_id INTEGER);
CREATE TRIGGER log_insert AFTER INSERT ON orders FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END;
INSERT INTO orders VALUES (1, 100);
.exit
""".trimIndent()
)
val afterFirstReopen = runScript(dir, "SELECT order_id FROM audit;\n.exit\n")
assertContains(afterFirstReopen, "1 row")
val afterSecondReopen = runScript(
dir,
"INSERT INTO orders VALUES (2, 200);\nSELECT order_id FROM audit ORDER BY order_id;\n.exit\n"
)
assertContains(afterSecondReopen, "2 rows")
}
/**
* The dumped `CREATE SEQUENCE` must bake in the sequence's CURRENT position (via `START WITH`),
* not the one it was originally created with - otherwise a checkpoint reopen would reissue
* already-handed-out values, breaking the one guarantee a sequence exists to provide.
*/
@Test
fun sequencePositionSurvivesReopenAfterCheckpointInsteadOfRestarting() {
val dir = tempDir()
runScript(
dir,
"""
CREATE SEQUENCE seq;
CREATE TABLE t (id INTEGER);
INSERT INTO t VALUES (NEXTVAL('seq')), (NEXTVAL('seq'));
.exit
""".trimIndent()
)
val reopened = runScript(dir, "INSERT INTO t VALUES (NEXTVAL('seq'));\nSELECT id FROM t WHERE id = 3;\n.exit\n")
// A restart-from-scratch bug would reissue 1 as the third value (1, 2, 1) instead of
// continuing to 3 - this query would then find zero rows instead of one.
assertContains(reopened, "1 row")
}
@Test
fun sequenceCreationAndNextvalDefaultSurviveReopenWithoutACheckpointViaWalReplay() {
val dir = tempDir()
val persistence = Persistence(dir)
persistence.appendWal(
listOf(
"CREATE SEQUENCE order_ids;",
"CREATE TABLE orders (id INTEGER DEFAULT NEXTVAL('order_ids'), item TEXT);",
"INSERT INTO orders (item) VALUES ('a');"
)
)
val output = StringBuilder()
Repl(output, output, Persistence(dir)).run(
"INSERT INTO orders (item) VALUES ('b');\nSELECT item FROM orders WHERE id = 1;\nSELECT item FROM orders WHERE id = 2;\n.exit\n"
.reader().buffered()
)
val text = output.toString()
assertContains(text, "a")
assertContains(text, "b")
assertEquals(2, text.lines().count { it.trim() == "1 row" })
}
@Test
fun dropSequenceSurvivesReopenAfterCheckpoint() {
val dir = tempDir()
runScript(
dir,
"""
CREATE SEQUENCE seq;
DROP SEQUENCE seq;
.exit
""".trimIndent()
)
val reopened = runScript(dir, ".sequences\n.exit\n")
assertContains(reopened, "no sequences")
}
/**
* PREPARE/EXECUTE themselves are session-scoped and never written to the WAL (see
* PrepareStatement's own doc) - but an INSERT run THROUGH EXECUTE is real data and must survive
* a reopen exactly like one typed literally would. This is the whole reason
* [kestrel.cli.Repl.executeBuffer] persists EXECUTE's RESOLVED statement instead of its literal
* `EXECUTE name(...)` text: that literal text would be meaningless to replay against a fresh
* session that never re-ran the PREPARE.
*/
@Test
fun executeOfAPreparedInsertPersistsTheResolvedRowAcrossReopen() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);
PREPARE addUser AS INSERT INTO users VALUES ($1, $2, $3);
EXECUTE addUser(1, 'ada', 36);
EXECUTE addUser(2, 'linus', 54);
.exit
""".trimIndent()
)
val reopened = runScript(dir, "SELECT name, age FROM users ORDER BY id ASC;\n.exit\n")
assertContains(reopened, "ada")
assertContains(reopened, "linus")
}
@Test
fun preparedStatementsDoNotSurviveReopen() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
PREPARE addUser AS INSERT INTO users VALUES ($1, $2);
.exit
""".trimIndent()
)
val reopened = runScript(dir, "EXECUTE addUser(1, 'ada');\n.exit\n")
assertContains(reopened, "Error: no such prepared statement: addUser")
}
@Test
fun temporaryTableDoesNotSurviveReopenAfterCheckpointButARegularTableAlongsideItDoes() {
val dir = tempDir()
runScript(
dir,
"""
CREATE TABLE regular (id INTEGER PRIMARY KEY, note TEXT);
} }