Store post state as text so Go owns the allowed values and future states such as locked remain representable without a database enum migration.
This commit is contained in:
@@ -145,9 +145,9 @@ VALUES ('homeowner', 'question-1', -1)`); err == nil {
|
||||
}
|
||||
|
||||
var rootParent sql.NullString
|
||||
var rootAuthor, title, rootBody, city, postDate, rootCreated, rootUpdated string
|
||||
var rootAuthor, title, rootBody, city, postDate, rootState, rootCreated, rootUpdated string
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT parent_id, author_id, title, body, city, post_date, created_at, updated_at
|
||||
SELECT parent_id, author_id, title, body, city, post_date, post_state, created_at, updated_at
|
||||
FROM posts
|
||||
WHERE id = 'question-1'`).Scan(
|
||||
&rootParent,
|
||||
@@ -156,6 +156,7 @@ WHERE id = 'question-1'`).Scan(
|
||||
&rootBody,
|
||||
&city,
|
||||
&postDate,
|
||||
&rootState,
|
||||
&rootCreated,
|
||||
&rootUpdated,
|
||||
); err != nil {
|
||||
@@ -167,19 +168,21 @@ WHERE id = 'question-1'`).Scan(
|
||||
rootBody != "It drips." ||
|
||||
city != "Oakland" ||
|
||||
postDate != "2026-08-26" ||
|
||||
rootState != "visible" ||
|
||||
rootCreated != "2026-08-26T08:00:00Z" ||
|
||||
rootUpdated != rootCreated {
|
||||
t.Fatalf("unexpected root post")
|
||||
}
|
||||
|
||||
var replyParent, replyAuthor, replyBody, replyCreated, replyUpdated string
|
||||
var replyParent, replyAuthor, replyBody, replyState, replyCreated, replyUpdated string
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT parent_id, author_id, body, created_at, updated_at
|
||||
SELECT parent_id, author_id, body, post_state, created_at, updated_at
|
||||
FROM posts
|
||||
WHERE id = 'answer:question-1'`).Scan(
|
||||
&replyParent,
|
||||
&replyAuthor,
|
||||
&replyBody,
|
||||
&replyState,
|
||||
&replyCreated,
|
||||
&replyUpdated,
|
||||
); err != nil {
|
||||
@@ -188,6 +191,7 @@ WHERE id = 'answer:question-1'`).Scan(
|
||||
if replyParent != "question-1" ||
|
||||
replyAuthor != "plumber" ||
|
||||
replyBody != "Replace the cartridge." ||
|
||||
replyState != "visible" ||
|
||||
replyCreated != "2026-08-26T09:00:00Z" ||
|
||||
replyUpdated != "2026-08-26T09:05:00Z" {
|
||||
t.Fatalf("unexpected reply post")
|
||||
@@ -205,9 +209,10 @@ WHERE user_id = 'homeowner' AND post_id = 'question-1'`).Scan(&voteValue); err !
|
||||
|
||||
if _, err := conn.ExecContext(ctx, `
|
||||
INSERT INTO posts (
|
||||
id, parent_id, author_id, title, body, city, post_date, hidden, created_at, updated_at
|
||||
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', '', '', 0, 'now', 'now'
|
||||
'invalid-reply', 'question-1', 'homeowner', 'Replies cannot have titles', 'Body', '', '',
|
||||
'visible', 'now', 'now'
|
||||
)`); err == nil {
|
||||
t.Fatal("reply with root-only title unexpectedly succeeded")
|
||||
}
|
||||
@@ -218,6 +223,7 @@ INSERT INTO posts (
|
||||
ParentID: sql.NullString{String: "answer:question-1", Valid: true},
|
||||
AuthorID: "homeowner",
|
||||
Body: "It is still dripping.",
|
||||
PostState: string(PostStateVisible),
|
||||
CreatedAt: "2026-08-26T10:00:00Z",
|
||||
UpdatedAt: "2026-08-26T10:00:00Z",
|
||||
}); err != nil {
|
||||
@@ -248,16 +254,18 @@ INSERT INTO posts (
|
||||
t.Fatalf("update rows=%d error=%v", n, err)
|
||||
}
|
||||
if n, err := queries.UpsertPostVoteOnVisibleRoot(ctx, sqlc.UpsertPostVoteOnVisibleRootParams{
|
||||
UserID: "plumber",
|
||||
PostID: "question-1",
|
||||
Value: 1,
|
||||
UserID: "plumber",
|
||||
PostID: "question-1",
|
||||
Value: 1,
|
||||
HiddenState: string(PostStateHidden),
|
||||
}); err != nil || n != 1 {
|
||||
t.Fatalf("vote rows=%d error=%v", n, err)
|
||||
}
|
||||
roots, err := queries.ListRootPosts(ctx, sqlc.ListRootPostsParams{
|
||||
ViewerID: "plumber",
|
||||
RowLimit: 100,
|
||||
PostDate: "2026-08-26",
|
||||
ViewerID: "plumber",
|
||||
RowLimit: 100,
|
||||
PostDate: "2026-08-26",
|
||||
HiddenState: string(PostStateHidden),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -273,7 +281,7 @@ INSERT INTO posts (
|
||||
DROP INDEX idx_posts_root_date;
|
||||
ALTER TABLE posts RENAME COLUMN post_date TO hunt_date;
|
||||
CREATE INDEX idx_posts_root_hunt
|
||||
ON posts(hunt_date, hidden)
|
||||
ON posts(hunt_date, post_state)
|
||||
WHERE parent_id IS NULL;`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -322,6 +330,84 @@ SELECT post_date FROM posts WHERE id = 'question-1'`).Scan(&migratedPostDate); e
|
||||
migratedPostDate,
|
||||
)
|
||||
}
|
||||
|
||||
if _, err := conn.ExecContext(ctx, `
|
||||
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;
|
||||
ALTER TABLE posts DROP COLUMN post_state;
|
||||
ALTER TABLE posts ADD CONSTRAINT posts_check CHECK (
|
||||
(parent_id IS NULL AND title <> '' AND post_date <> '')
|
||||
OR
|
||||
(parent_id IS NOT NULL AND title = '' AND city = '' AND post_date = '' AND hidden = 0)
|
||||
);
|
||||
CREATE INDEX idx_posts_root_date
|
||||
ON posts(post_date, hidden)
|
||||
WHERE parent_id IS NULL;`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migratePostState(ctx, conn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migratePostState(ctx, conn); err != nil {
|
||||
t.Fatalf("post state migration is not idempotent: %v", err)
|
||||
}
|
||||
|
||||
var postStateColumnCount, hiddenColumnCount int
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
count(*) FILTER (WHERE column_name = 'post_state'),
|
||||
count(*) FILTER (WHERE column_name = 'hidden')
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'posts'`).Scan(&postStateColumnCount, &hiddenColumnCount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var hiddenState, replyStateAfterMigration string
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT post_state FROM posts WHERE id = 'question-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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var postStateDataType string
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'posts'
|
||||
AND column_name = 'post_state'`).Scan(&postStateDataType); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stateIndexCount int
|
||||
if err := conn.QueryRowContext(ctx, `
|
||||
SELECT count(*)
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = current_schema()
|
||||
AND tablename = 'posts'
|
||||
AND indexname = 'idx_posts_root_date'
|
||||
AND indexdef LIKE '%(post_date, post_state)%'`).Scan(&stateIndexCount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if postStateColumnCount != 1 ||
|
||||
hiddenColumnCount != 0 ||
|
||||
hiddenState != "hidden" ||
|
||||
replyStateAfterMigration != "visible" ||
|
||||
postStateDataType != "text" ||
|
||||
stateIndexCount != 1 {
|
||||
t.Fatalf(
|
||||
"post state migration columns=%d hidden_columns=%d root=%q reply=%q type=%q indexes=%d",
|
||||
postStateColumnCount,
|
||||
hiddenColumnCount,
|
||||
hiddenState,
|
||||
replyStateAfterMigration,
|
||||
postStateDataType,
|
||||
stateIndexCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigratePostsReportsStep(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user