Remove legacy question storage
CI / test (pull_request) Failing after 5m44s

This commit is contained in:
2026-08-27 09:08:40 -07:00
parent f0591ccea3
commit df17e0e6f3
19 changed files with 125 additions and 1392 deletions
+75 -75
View File
@@ -13,7 +13,7 @@ import (
"plumber/internal/store/sqlc"
)
func TestMigratePostsCopiesLegacyData(t *testing.T) {
func TestPostMigrationsAndQueries(t *testing.T) {
rawURL := strings.TrimSpace(os.Getenv("TEST_DATABASE_URL"))
if rawURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
@@ -47,47 +47,13 @@ func TestMigratePostsCopiesLegacyData(t *testing.T) {
t.Fatal(err)
}
legacySchema := `
baseSchema := `
CREATE TABLE users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
role TEXT NOT NULL
);
CREATE TABLE questions (
id TEXT PRIMARY KEY,
author_id TEXT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
body TEXT NOT NULL,
city TEXT NOT NULL DEFAULT '',
hunt_date TEXT NOT NULL,
hidden INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE answers (
question_id TEXT PRIMARY KEY REFERENCES questions(id) ON DELETE CASCADE,
author_id TEXT NOT NULL REFERENCES users(id),
body TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE votes (
user_id TEXT NOT NULL REFERENCES users(id),
question_id TEXT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
value INTEGER NOT NULL CHECK (value IN (-1, 1)),
PRIMARY KEY (user_id, question_id)
);`
if err := applySchema(ctx, conn, legacySchema); err != nil {
t.Fatal(err)
}
if _, err := conn.ExecContext(ctx, `
INSERT INTO users (id, name, role)
VALUES ('homeowner', 'Home Owner', 'user'), ('plumber', 'The Plumber', 'admin');
INSERT INTO questions (id, author_id, title, body, city, hunt_date, hidden, created_at)
VALUES ('question-1', 'homeowner', 'Leaky sink', 'It drips.', 'Oakland', '2026-08-26', 0, '2026-08-26T08:00:00Z');
INSERT INTO answers (question_id, author_id, body, created_at, updated_at)
VALUES ('question-1', 'plumber', 'Replace the cartridge.', '2026-08-26T09:00:00Z', '2026-08-26T09:05:00Z');
INSERT INTO votes (user_id, question_id, value)
VALUES ('homeowner', 'question-1', 1);`); err != nil {
if err := applySchema(ctx, conn, baseSchema); err != nil {
t.Fatal(err)
}
@@ -103,28 +69,40 @@ VALUES ('homeowner', 'question-1', 1);`); err != nil {
if err := migratePostVoteIndex(ctx, conn); err != nil {
t.Fatalf("post vote index migration is not idempotent: %v", err)
}
if err := migratePostAuthorIndex(ctx, conn); err != nil {
t.Fatal(err)
}
if err := migratePostAuthorIndex(ctx, conn); err != nil {
t.Fatalf("post author index migration is not idempotent: %v", err)
}
if _, err := conn.ExecContext(ctx, `
INSERT INTO users (id, name, role)
VALUES ('homeowner', 'Home Owner', 'user'), ('plumber', 'The Plumber', 'admin');
INSERT INTO posts (
id, parent_id, author_id, title, body, city, post_date, post_state, created_at, updated_at
) VALUES
(
'root-1', NULL, 'homeowner', 'Leaky sink', 'It drips.', 'Oakland',
'2026-08-26', 'visible', '2026-08-26T08:00:00Z', '2026-08-26T08:00:00Z'
),
(
'reply-1', 'root-1', 'plumber', '', 'Replace the cartridge.', '', '',
'visible', '2026-08-26T09:00:00Z', '2026-08-26T09:05:00Z'
);
INSERT INTO post_votes (user_id, post_id, value)
VALUES ('homeowner', 'root-1', 1);`); err != nil {
t.Fatal(err)
}
var postCount, voteCount, legacyQuestionCount, legacyAnswerCount int
var postCount, voteCount int
if err := conn.QueryRowContext(ctx, "SELECT count(*) FROM posts").Scan(&postCount); err != nil {
t.Fatal(err)
}
if err := conn.QueryRowContext(ctx, "SELECT count(*) FROM post_votes").Scan(&voteCount); err != nil {
t.Fatal(err)
}
if err := conn.QueryRowContext(ctx, "SELECT count(*) FROM questions").Scan(&legacyQuestionCount); err != nil {
t.Fatal(err)
}
if err := conn.QueryRowContext(ctx, "SELECT count(*) FROM answers").Scan(&legacyAnswerCount); err != nil {
t.Fatal(err)
}
if postCount != 2 || voteCount != 1 || legacyQuestionCount != 1 || legacyAnswerCount != 1 {
t.Fatalf(
"counts posts=%d votes=%d legacy questions=%d answers=%d",
postCount,
voteCount,
legacyQuestionCount,
legacyAnswerCount,
)
if postCount != 2 || voteCount != 1 {
t.Fatalf("counts posts=%d votes=%d", postCount, voteCount)
}
var postVoteIndexCount int
if err := conn.QueryRowContext(ctx, `
@@ -152,7 +130,7 @@ WHERE schemaname = current_schema()
}
if _, err := conn.ExecContext(ctx, `
INSERT INTO post_votes (user_id, post_id, value)
VALUES ('homeowner', 'question-1', -1)`); err == nil {
VALUES ('homeowner', 'root-1', -1)`); err == nil {
t.Fatal("duplicate user/post vote unexpectedly succeeded")
}
@@ -161,7 +139,7 @@ VALUES ('homeowner', 'question-1', -1)`); err == nil {
if err := conn.QueryRowContext(ctx, `
SELECT parent_id, author_id, title, body, city, post_date, post_state, created_at, updated_at
FROM posts
WHERE id = 'question-1'`).Scan(
WHERE id = 'root-1'`).Scan(
&rootParent,
&rootAuthor,
&title,
@@ -190,7 +168,7 @@ WHERE id = 'question-1'`).Scan(
if err := conn.QueryRowContext(ctx, `
SELECT parent_id, author_id, body, post_state, created_at, updated_at
FROM posts
WHERE id = 'answer:question-1'`).Scan(
WHERE id = 'reply-1'`).Scan(
&replyParent,
&replyAuthor,
&replyBody,
@@ -200,7 +178,7 @@ WHERE id = 'answer:question-1'`).Scan(
); err != nil {
t.Fatal(err)
}
if replyParent != "question-1" ||
if replyParent != "root-1" ||
replyAuthor != "plumber" ||
replyBody != "Replace the cartridge." ||
replyState != "visible" ||
@@ -212,7 +190,7 @@ WHERE id = 'answer:question-1'`).Scan(
var voteValue int
if err := conn.QueryRowContext(ctx, `
SELECT value FROM post_votes
WHERE user_id = 'homeowner' AND post_id = 'question-1'`).Scan(&voteValue); err != nil {
WHERE user_id = 'homeowner' AND post_id = 'root-1'`).Scan(&voteValue); err != nil {
t.Fatal(err)
}
if voteValue != 1 {
@@ -223,7 +201,7 @@ WHERE user_id = 'homeowner' AND post_id = 'question-1'`).Scan(&voteValue); err !
INSERT INTO posts (
id, parent_id, author_id, title, body, city, post_date, post_state, created_at, updated_at
) VALUES (
'invalid-reply', 'question-1', 'homeowner', 'Replies cannot have titles', 'Body', '', '',
'invalid-reply', 'root-1', 'homeowner', 'Replies cannot have titles', 'Body', '', '',
'visible', 'now', 'now'
)`); err == nil {
t.Fatal("reply with root-only title unexpectedly succeeded")
@@ -232,7 +210,7 @@ INSERT INTO posts (
queries := sqlc.New(conn)
if err := queries.CreatePost(ctx, sqlc.CreatePostParams{
ID: "follow-up",
ParentID: sql.NullString{String: "answer:question-1", Valid: true},
ParentID: sql.NullString{String: "reply-1", Valid: true},
AuthorID: "homeowner",
Body: "It is still dripping.",
PostState: string(PostStateVisible),
@@ -241,17 +219,17 @@ INSERT INTO posts (
}); err != nil {
t.Fatal(err)
}
thread, err := queries.ListPostThread(ctx, "question-1")
thread, err := queries.ListPostThread(ctx, "root-1")
if err != nil {
t.Fatal(err)
}
if len(thread) != 3 ||
thread[0].ID != "question-1" ||
thread[1].ID != "answer:question-1" ||
thread[0].ID != "root-1" ||
thread[1].ID != "reply-1" ||
thread[2].ID != "follow-up" {
t.Fatalf("recursive thread = %+v", thread)
}
nonRootThread, err := queries.ListPostThread(ctx, "answer:question-1")
nonRootThread, err := queries.ListPostThread(ctx, "reply-1")
if err != nil {
t.Fatal(err)
}
@@ -267,7 +245,7 @@ INSERT INTO posts (
}
if n, err := queries.UpsertPostVoteOnVisibleRoot(ctx, sqlc.UpsertPostVoteOnVisibleRootParams{
UserID: "plumber",
PostID: "question-1",
PostID: "root-1",
Value: 1,
HiddenState: string(PostStateHidden),
}); err != nil || n != 1 {
@@ -290,7 +268,7 @@ INSERT INTO posts (
}
summary, err := queries.GetRootPostVoteSummary(ctx, sqlc.GetRootPostVoteSummaryParams{
ViewerID: "plumber",
RootID: "question-1",
RootID: "root-1",
})
if err != nil || summary.Score != 2 || summary.UserVote != 1 {
t.Fatalf("root vote summary = %+v, %v", summary, err)
@@ -300,7 +278,7 @@ INSERT INTO posts (
HiddenState: string(PostStateHidden),
RowLimit: 50,
})
if err != nil || len(byAuthor) != 1 || byAuthor[0].ID != "question-1" {
if err != nil || len(byAuthor) != 1 || byAuthor[0].ID != "root-1" {
t.Fatalf("roots by author = %+v, %v", byAuthor, err)
}
answeredBy, err := queries.ListRootPostsAnsweredBy(ctx, sqlc.ListRootPostsAnsweredByParams{
@@ -308,14 +286,14 @@ INSERT INTO posts (
AdminID: "plumber",
RowLimit: 50,
})
if err != nil || len(answeredBy) != 1 || answeredBy[0].ID != "question-1" {
if err != nil || len(answeredBy) != 1 || answeredBy[0].ID != "root-1" {
t.Fatalf("roots answered by admin = %+v, %v", answeredBy, err)
}
for _, state := range []PostState{PostStateLocked, PostStateVisible} {
n, err := queries.UpdateRootPostState(ctx, sqlc.UpdateRootPostStateParams{
PostState: string(state),
UpdatedAt: "2026-08-26T10:10:00Z",
ID: "question-1",
ID: "root-1",
})
if err != nil || n != 1 {
t.Fatalf("set root state %q rows=%d error=%v", state, n, err)
@@ -358,7 +336,7 @@ WHERE schemaname = current_schema()
}
var migratedPostDate string
if err := conn.QueryRowContext(ctx, `
SELECT post_date FROM posts WHERE id = 'question-1'`).Scan(&migratedPostDate); err != nil {
SELECT post_date FROM posts WHERE id = 'root-1'`).Scan(&migratedPostDate); err != nil {
t.Fatal(err)
}
if postDateColumnCount != 1 ||
@@ -380,7 +358,7 @@ SELECT post_date FROM posts WHERE id = 'question-1'`).Scan(&migratedPostDate); e
DROP INDEX idx_posts_root_date;
ALTER TABLE posts DROP CONSTRAINT posts_shape_check;
ALTER TABLE posts ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;
UPDATE posts SET hidden = CASE WHEN id = 'question-1' THEN 1 ELSE 0 END;
UPDATE posts SET hidden = CASE WHEN id = 'root-1' THEN 1 ELSE 0 END;
ALTER TABLE posts DROP COLUMN post_state;
ALTER TABLE posts ADD CONSTRAINT posts_check CHECK (
(parent_id IS NULL AND title <> '' AND post_date <> '')
@@ -411,11 +389,11 @@ WHERE table_schema = current_schema()
}
var hiddenState, replyStateAfterMigration string
if err := conn.QueryRowContext(ctx, `
SELECT post_state FROM posts WHERE id = 'question-1'`).Scan(&hiddenState); err != nil {
SELECT post_state FROM posts WHERE id = 'root-1'`).Scan(&hiddenState); err != nil {
t.Fatal(err)
}
if err := conn.QueryRowContext(ctx, `
SELECT post_state FROM posts WHERE id = 'answer:question-1'`).Scan(&replyStateAfterMigration); err != nil {
SELECT post_state FROM posts WHERE id = 'reply-1'`).Scan(&replyStateAfterMigration); err != nil {
t.Fatal(err)
}
var postStateDataType string
@@ -453,15 +431,37 @@ WHERE schemaname = current_schema()
stateIndexCount,
)
}
if _, err := conn.ExecContext(ctx, `
CREATE TABLE questions (id TEXT PRIMARY KEY);
CREATE TABLE votes (id TEXT PRIMARY KEY);
CREATE TABLE answers (id TEXT PRIMARY KEY);`); err != nil {
t.Fatal(err)
}
if err := migrateDropLegacyPostTables(ctx, conn); err != nil {
t.Fatal(err)
}
if err := migrateDropLegacyPostTables(ctx, conn); err != nil {
t.Fatalf("legacy table cleanup is not idempotent: %v", err)
}
for _, table := range []string{"questions", "votes", "answers"} {
var relation sql.NullString
if err := conn.QueryRowContext(ctx, "SELECT to_regclass($1)", table).Scan(&relation); err != nil {
t.Fatal(err)
}
if relation.Valid {
t.Fatalf("legacy table %q still exists", table)
}
}
}
func TestMigratePostsReportsStep(t *testing.T) {
t.Parallel()
exec := &failingMigrationExec{failAt: 6}
exec := &failingMigrationExec{failAt: 5}
err := migratePosts(context.Background(), exec)
if err == nil || !strings.Contains(err.Error(), "copy questions") {
t.Fatalf("error = %v, want copy questions context", err)
if err == nil || !strings.Contains(err.Error(), "create post votes") {
t.Fatalf("error = %v, want create post votes context", err)
}
}