diff --git a/src/test/kotlin/kestrel/PersistenceTest.kt b/src/test/kotlin/kestrel/PersistenceTest.kt index 3066154..0ebc487 100644 --- a/src/test/kotlin/kestrel/PersistenceTest.kt +++ b/src/test/kotlin/kestrel/PersistenceTest.kt @@ -891,4 +891,159 @@ class PersistenceTest { dir, """ CREATE TABLE regular (id INTEGER PRIMARY KEY, note TEXT); + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, note TEXT); + INSERT INTO regular VALUES (1, 'keepsMe'); + INSERT INTO scratch VALUES (1, 'losesMe'); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT note FROM regular;\nSELECT note FROM scratch;\n.exit\n") + assertContains(reopened, "keepsMe") + assertContains(reopened, "Error: no such table: scratch") + assertTrue(!reopened.contains("losesMe"), "a TEMP table's rows must not survive reopen") + } + + @Test + fun renamingATemporaryTableKeepsItTemporarySoTheRenamedNameAlsoDoesNotSurviveReopen() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY); + ALTER TABLE scratch RENAME TO renamedScratch; + INSERT INTO renamedScratch VALUES (1); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id FROM renamedScratch;\n.exit\n") + assertContains(reopened, "Error: no such table: renamedScratch") + } + + @Test + fun addColumnAndDropColumnOnATemporaryTableKeepItTemporaryAcrossReopen() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, a INTEGER); + ALTER TABLE scratch ADD COLUMN b TEXT; + ALTER TABLE scratch DROP COLUMN a; + INSERT INTO scratch VALUES (1, 'x'); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id FROM scratch;\n.exit\n") + assertContains(reopened, "Error: no such table: scratch") + } + + @Test + fun renamingAColumnOnATemporaryTableKeepsItTemporaryAcrossReopen() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, a INTEGER); + ALTER TABLE scratch RENAME COLUMN a TO b; + INSERT INTO scratch VALUES (1, 2); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id FROM scratch;\n.exit\n") + assertContains(reopened, "Error: no such table: scratch") + } + + @Test + fun aTemporaryTablesForeignKeyBeingDroppedByAnUnrelatedCascadeKeepsItTemporaryAcrossReopen() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TABLE parent (id INTEGER PRIMARY KEY); + CREATE TEMP TABLE child (id INTEGER PRIMARY KEY, parentId INTEGER, FOREIGN KEY (parentId) REFERENCES parent(id)); + DROP TABLE parent CASCADE; + INSERT INTO child VALUES (1, NULL); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id FROM child;\n.exit\n") + assertContains(reopened, "Error: no such table: child") + } + + @Test + fun anIndexOnATemporaryTableNeverPersists() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, tag TEXT); + CREATE INDEX scratchTag ON scratch (tag); + INSERT INTO scratch VALUES (1, 'x'); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id FROM scratch;\n.indexes\n.exit\n") + assertContains(reopened, "Error: no such table: scratch") + assertTrue(!reopened.contains("scratchTag"), "an index on a TEMP table must not survive reopen either") + } + + @Test + fun droppingAnIndexOnATemporaryTableNeverReachesTheIncrementalWalEitherSideOfIt() { + val dir = tempDir() + runScriptThatCrashesBeforeItCanCheckpoint( + dir, + """ + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, tag TEXT); + CREATE INDEX scratchTag ON scratch (tag); + DROP INDEX scratchTag; + INSERT INTO scratch VALUES (1, 'x'); + """.trimIndent() + "\n" + ) + val reopened = runScript(dir, "SELECT id FROM scratch;\n.indexes\n.exit\n") + assertContains(reopened, "Error: no such table: scratch") + assertTrue(!reopened.contains("scratchTag")) + } + + @Test + fun aTemporaryTableNeverReachesTheIncrementalWriteAheadLogEvenBeforeACheckpoint() { + val dir = tempDir() + runScriptThatCrashesBeforeItCanCheckpoint( + dir, + """ + CREATE TABLE regular (id INTEGER PRIMARY KEY); + CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY); + INSERT INTO regular VALUES (111); + INSERT INTO scratch VALUES (222); + """.trimIndent() + "\n" + ) + // No checkpoint ever ran, so this reopen replays raw incremental WAL entries only - if the + // TEMP table's CREATE/INSERT had leaked into that log, replaying "INSERT INTO scratch..." + // would either resurrect it or (if its CREATE alone were skipped but not its INSERT) throw + // "no such table" loudly enough to show up here. + val reopened = runScript(dir, "SELECT id FROM regular;\nSELECT id FROM scratch;\n.exit\n") + assertContains(reopened, "111") + assertContains(reopened, "Error: no such table: scratch") + assertTrue(!reopened.contains("222"), "a TEMP table's row must never reach the WAL, crash or not") + } + + @Test + fun aTruncateNamingBothATemporaryAndARegularTableStillPersistsTheRegularTablesTruncationBeforeACheckpoint() { + val dir = tempDir() + runScriptThatCrashesBeforeItCanCheckpoint( + dir, + """ + CREATE TABLE regular (id INTEGER); + CREATE TEMP TABLE scratch (id INTEGER); + INSERT INTO regular VALUES (111), (222); + INSERT INTO scratch VALUES (333); + TRUNCATE TABLE regular, scratch; + """.trimIndent() + "\n" + ) + // If the TEMP name were not stripped out of the persisted TRUNCATE text, replaying + // "TRUNCATE TABLE regular, scratch" against a reload where scratch was never recreated would + // throw before truncating ANYTHING (Catalog.truncateTables validates every name up front) - + // silently leaving regular's rows intact instead of empty, since replay errors are swallowed + // (see Repl.printResult's `if (replaying) return`). + val reopened = runScript(dir, "SELECT id FROM regular;\n.exit\n") + assertTrue(!reopened.contains("111") && !reopened.contains("222"), "regular's TRUNCATE must survive the crash-before-checkpoint replay") + } }