test: cover renameTablePropagatesIntoAViewsDistinctOnSubquery
This commit is contained in:
1 parent
118d58728f
commit
965cc2974d
1 file changed
+173
@@ -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);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user