fix: widen ce_sessions.last_activity_at to bigint so PG first-boot migration survives epoch-ms values
project.ce_sessions.last_activity_at stores Date.now() epoch milliseconds but was declared integer in both the Drizzle shape and the CE plugin schema-hook DDL, overflowing PG int4 during the SQLite -> PostgreSQL first-boot auto-migration and blocking startup at task-store init. Now bigint in both sites, with an idempotent ALTER for datadirs that already materialized the integer column, plus a schema-wide invariant test that no numeric *_at/*_time/*_timestamp column is 32-bit integer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/pg-ce-sessions-bigint-last-activity.md
Normal file
7
.changeset/pg-ce-sessions-bigint-last-activity.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix startup failure where the SQLite → PostgreSQL migration aborted on CE session timestamps.
|
||||
category: fix
|
||||
dev: project.ce_sessions.last_activity_at was `integer` but stores epoch milliseconds, overflowing PG int4 and failing first-boot auto-migration. Now `bigint` in the Drizzle shape and CE plugin schema-hook DDL, with an idempotent `ALTER COLUMN ... TYPE bigint` for datadirs that already materialized the integer column.
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
FNXC:PostgresSchema 2026-07-13-19:40:
|
||||
Regression invariant for the SQLite → PostgreSQL first-boot migration failure
|
||||
where project.ce_sessions.last_activity_at was declared `integer` but stores
|
||||
epoch milliseconds (Date.now() ≈ 1.78e12), overflowing PG integer (max ~2.1e9)
|
||||
and aborting startup ("value ... is out of range for type integer").
|
||||
|
||||
Invariant across ALL schemas (project/central/archive/plugin): a numeric column
|
||||
whose name marks it as a point-in-time value (`*_at`, `*_time`, `*_timestamp`)
|
||||
must be bigint, never 32-bit integer. Timestamp columns stored as ISO text are
|
||||
fine; durations/counters (`*_ms` intervals, counts) fit integer and are exempt.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getTableColumns, getTableName } from "drizzle-orm";
|
||||
import { PgTable } from "drizzle-orm/pg-core";
|
||||
import * as schema from "../../postgres/schema/index.js";
|
||||
|
||||
const TIMESTAMP_NAME = /(_at|_time|_timestamp)$/;
|
||||
|
||||
function collectTables(mod: Record<string, unknown>): PgTable[] {
|
||||
return Object.values(mod).filter((v): v is PgTable => v instanceof PgTable);
|
||||
}
|
||||
|
||||
describe("PG schema epoch-ms columns", () => {
|
||||
it("declares every numeric *_at/*_time/*_timestamp column as bigint, not integer", () => {
|
||||
const offenders: string[] = [];
|
||||
for (const mod of [schema.project, schema.central, schema.archive, schema.plugin]) {
|
||||
for (const table of collectTables(mod as Record<string, unknown>)) {
|
||||
for (const column of Object.values(getTableColumns(table))) {
|
||||
if (!TIMESTAMP_NAME.test(column.name)) continue;
|
||||
if (column.getSQLType() === "integer") {
|
||||
offenders.push(`${getTableName(table)}.${column.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -107,10 +107,16 @@ export const cePluginSchemaInit: PluginSchemaInitHook = {
|
||||
artifact_path text,
|
||||
error text,
|
||||
turn_interval_ms integer NOT NULL DEFAULT 120000,
|
||||
last_activity_at integer NOT NULL,
|
||||
last_activity_at bigint NOT NULL,
|
||||
created_at text NOT NULL,
|
||||
updated_at text NOT NULL
|
||||
);
|
||||
-- FNXC:PostgresSchema 2026-07-13-19:35:
|
||||
-- last_activity_at holds epoch milliseconds (Date.now()), which overflows
|
||||
-- PG integer. Datadirs created before this fix materialized the column as
|
||||
-- integer via the CREATE TABLE IF NOT EXISTS above, so widen it in place.
|
||||
-- Idempotent: ALTER ... TYPE bigint on an already-bigint column is a no-op.
|
||||
ALTER TABLE project.ce_sessions ALTER COLUMN last_activity_at TYPE bigint;
|
||||
CREATE INDEX IF NOT EXISTS "idxCeSessionsStatusUpdated"
|
||||
ON project.ce_sessions(status, updated_at DESC, id);
|
||||
CREATE INDEX IF NOT EXISTS "idxCeSessionsStageCreated"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* while still materializing on a fresh database.
|
||||
*/
|
||||
|
||||
import { text, integer, boolean, foreignKey, index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { text, integer, bigint, boolean, foreignKey, index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { projectSchema } from "./project.js";
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,14 @@ export const ceSessions = projectSchema.table("ce_sessions", {
|
||||
artifactPath: text("artifact_path"),
|
||||
error: text("error"),
|
||||
turnIntervalMs: integer("turn_interval_ms").notNull().default(120000),
|
||||
lastActivityAt: integer("last_activity_at").notNull(),
|
||||
/*
|
||||
FNXC:PostgresSchema 2026-07-13-19:35:
|
||||
last_activity_at stores epoch milliseconds (Date.now()), which overflows PG
|
||||
integer (max ~2.1e9) — epoch-ms values are ~1.78e12. Must be bigint like the
|
||||
other epoch-ms columns (see project.ts pr_* tables). An integer column here
|
||||
broke the SQLite → PostgreSQL first-boot auto-migration and blocked startup.
|
||||
*/
|
||||
lastActivityAt: bigint("last_activity_at", { mode: "number" }).notNull(),
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
}, (t) => [
|
||||
|
||||
Reference in New Issue
Block a user