feat(sync_jobs): prep sync_job_id for int4 → bigint widening (NAN-5491 Phase 0)#6260
Conversation
…1 Phase 0) Backward-compatible groundwork; all no-ops until the columns actually widen in later phases. - records upsert: cast sync_job_id as ::bigint (was ::integer) so values > 2^31 won't fail at the cast once the column is widened. - job.service: normalize sync_jobs.id to a JS number on read (node-pg returns bigint as a string) and assert it stays within Number.MAX_SAFE_INTEGER, so Job.id / nangoProps.syncJobId stay real numbers. - cap syncJobId at Number.MAX_SAFE_INTEGER in the persist param schemas and the nangoProps / lambda-runner schemas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 42cada2da7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ? '(?::integer, ?::text, ?::uuid, ?::text, ?::text, ?::uuid, ?::bigint, ?::timestamptz, ?::timestamptz)' | ||
| : '(?::integer, ?::text, ?::uuid, ?::text, ?::text, ?::uuid, ?::bigint, ?::timestamptz)' |
There was a problem hiding this comment.
Widen records_seen with the upsert path
When this path receives a sync_job_id above the int4 ceiling after records.sync_job_id is widened, the upsert still calls insertSeenEntry for every non-empty batch, and records_seen.sync_job_id is still declared as integer in packages/records/lib/db/migrations/20260512000100_create_records_seen.ts. That insert will reject the same >2^31 id even though this CTE now accepts it as bigint, so the promised large-id records upsert still fails unless the seen table/path is widened at the same time.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
1 issue found across 7 files
Confidence score: 3/5
- There is a concrete regression risk in
packages/shared/lib/services/sync/job.service.ts: coercingbiginttonumbercan round values aboveNumber.MAX_SAFE_INTEGER, which may cause updates to affect the wrong sync job. - Given the issue is medium severity (6/10) with high confidence (8/10) and directly user-impacting data behavior, this sits in a moderate-risk zone rather than a safe-to-merge baseline.
- Pay close attention to
packages/shared/lib/services/sync/job.service.ts- guard bigint-to-number conversion or enforce a DB-side cap to prevent ID precision loss.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/shared/lib/services/sync/job.service.ts">
<violation number="1" location="packages/shared/lib/services/sync/job.service.ts:16">
P2: Guard this bigint→number coercion. Without a database-side cap, ids above `Number.MAX_SAFE_INTEGER` will be rounded here and later updates can target the wrong sync job.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| // consumers (Job.id, nangoProps.syncJobId) expect a JS number. Coerce on read. id is a NOT NULL | ||
| // PK and the sequence is capped at Number.MAX_SAFE_INTEGER, so the value always fits exactly. | ||
| function normalizeSyncJobId<T extends { id: number }>(job: T): T { | ||
| return { ...job, id: Number(job.id) }; |
There was a problem hiding this comment.
P2: Guard this bigint→number coercion. Without a database-side cap, ids above Number.MAX_SAFE_INTEGER will be rounded here and later updates can target the wrong sync job.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/shared/lib/services/sync/job.service.ts, line 16:
<comment>Guard this bigint→number coercion. Without a database-side cap, ids above `Number.MAX_SAFE_INTEGER` will be rounded here and later updates can target the wrong sync job.</comment>
<file context>
@@ -9,6 +9,13 @@ import type { ConnectionJobs, SyncResultByModel } from '@nangohq/types';
+// consumers (Job.id, nangoProps.syncJobId) expect a JS number. Coerce on read. id is a NOT NULL
+// PK and the sequence is capped at Number.MAX_SAFE_INTEGER, so the value always fits exactly.
+function normalizeSyncJobId<T extends { id: number }>(job: T): T {
+ return { ...job, id: Number(job.id) };
+}
+
</file context>
| return { ...job, id: Number(job.id) }; | |
| const id = Number(job.id); | |
| if (!Number.isSafeInteger(id)) { | |
| throw new Error(`sync_jobs.id ${job.id} exceeds Number.MAX_SAFE_INTEGER`); | |
| } | |
| return { ...job, id }; |
There was a problem hiding this comment.
If we ever reach that situation we are fuck and we will require to change the contract and no longer use Number, also we should consider that took us 4 years to reach the limit of 2^32, the 2^53 will most likely never happen in the following 8.4 million years.
There was a problem hiding this comment.
Thanks for the context—understood.
There was a problem hiding this comment.
Im expecting that all of us will gone or have colonized Mars by that time
records.sync_job_id is unused (write-only since records_seen took over the generation logic), so it will be decoupled (write NULL) and dropped rather than widened — the cast is no longer needed. Keeps this PR to sync_jobs prep only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/records/lib/models/records.ts">
<violation number="1">
P1: `sync_job_id` is being cast back to `integer` here, so this upsert will still reject ids above the int4 limit once writers start producing bigint job ids.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| // consumers (Job.id, nangoProps.syncJobId) expect a JS number. Coerce on read. id is a NOT NULL | ||
| // PK and the sequence is capped at Number.MAX_SAFE_INTEGER, so the value always fits exactly. | ||
| function normalizeSyncJobId<T extends { id: number }>(job: T): T { | ||
| return { ...job, id: Number(job.id) }; |
Phase 0 groundwork for NAN-5491 — widening
sync_jobs.idfromint4(exhausting ~July 2026) tobigint. Everything here is backward-compatible and a no-op until the columns are actually widened in later phases. Full plan: Analysis: migration of sync_job_id.sync_job_idasbigint(wasinteger) so a value above the int4 ceiling won't be rejected at the cast once the column is widened. Safe today:bigint → int4is an implicit assignment cast for in-range values, and all current ids are in range.job.servicecoercessync_jobs.idto a JS number on read (node-pg returnsbigintcolumns as strings), keepingJob.id/nangoProps.syncJobIdreal numbers after the widening.syncJobIdatNumber.MAX_SAFE_INTEGERin the persist param schemas and thenangoProps/ lambda-runner schemas. We keepnumber(no global pg type parser), so 2^53−1 is the enforced ceiling — still ~4M× the current 2^31 limit.Deliberately deferred: narrowing
getRecordsToUpdate'sSELECT *to dropsync_job_id— it'd require changingdecryptRecordData's type and the fetched value is never read, so it's harmless; left as optional follow-up.Test plan
tsc -b— no type errors in the changed packages (shared,records,persist,jobs,lambda-runner)Job.idstays anumbersync_job_id > 2^31round-trips through the records upsert