/** * diagnose-ticket-varchar-overflow.ts * * One-shot: identify Autotask Tickets whose string fields exceed the * varchar() limits declared on the local postgres `tickets` table. * * Why: every full tickets sync since 2026-05-21 16:14 fails with * `value too long for type character varying(255)` * during bulkUpsert. The pg error doesn't name the column, so we replicate * the real sync's Autotask query EXACTLY (createDate filter, no IncludeFields) * and walk the response checking every string field on every record. * * Stops after first 10 violations found. * * Usage: * npx tsx scripts/diagnose-ticket-varchar-overflow.ts */ import { config } from 'dotenv'; import { resolve } from 'path'; config({ path: resolve(__dirname, '../.env.local') }); const API_BASE = process.env.AUTOTASK_API_URL!; const USERNAME = process.env.AUTOTASK_USERNAME!; const SECRET = process.env.AUTOTASK_SECRET!; const INT_CODE = process.env.AUTOTASK_API_INTEGRATION_CODE!; if (!API_BASE || !USERNAME || !SECRET || !INT_CODE) { console.error('Missing Autotask credentials in env (.env.local)'); process.exit(1); } // Postgres tickets table varchar column limits (keep in sync with schema). // Autotask camelCase field name -> postgres column limit. const FIELD_LIMITS: Record = { title: 255, purchaseOrderNumber: 100, ticketNumber: 100, changeInfoField1: 255, changeInfoField2: 255, changeInfoField3: 255, changeInfoField4: 255, changeInfoField5: 255, }; function authHeaders(): Record { return { Username: USERNAME, Secret: SECRET, APIIntegrationcode: INT_CODE, 'Content-Type': 'application/json', Accept: 'application/json', }; } interface Violation { ticketId: number; ticketNumber?: string; field: string; length: number; limit: number; preview: string; createDate?: string; } const MAX_VIOLATIONS = 10; async function main() { const since = new Date(); since.setUTCFullYear(since.getUTCFullYear() - 2); const sinceIso = since.toISOString(); console.log(`Scanning Tickets with createDate >= ${sinceIso} (matches real sync filter exactly)`); console.log(`Field limits: ${JSON.stringify(FIELD_LIMITS)}`); console.log(`Will also report ANY string field >255 chars even if not in known limit list.`); console.log(''); // Real sync sends ONLY {MaxRecords, filter} — no IncludeFields. Match exactly. const baseBody = { MaxRecords: 500, filter: [{ field: 'createDate', op: 'gte', value: sinceIso }], }; const violations: Violation[] = []; const unknownFieldOverflows: Violation[] = []; let totalScanned = 0; let pageIdx = 0; let url: string | null = `${API_BASE}/Tickets/query`; while (url) { pageIdx += 1; const res: Response = await fetch(url, { method: 'POST', headers: authHeaders(), body: JSON.stringify(baseBody), }); if (!res.ok) { const t = await res.text(); throw new Error(`Autotask query failed (page ${pageIdx}): ${res.status} ${t.slice(0, 500)}`); } const data = await res.json(); const items: any[] = data.items || []; totalScanned += items.length; for (const rec of items) { for (const [field, v] of Object.entries(rec)) { if (typeof v !== 'string') continue; const limit = FIELD_LIMITS[field]; if (limit !== undefined && v.length > limit) { violations.push({ ticketId: rec.id, ticketNumber: rec.ticketNumber, field, length: v.length, limit, preview: v.slice(0, 100) + (v.length > 100 ? '…' : ''), createDate: rec.createDate, }); } else if (limit === undefined && v.length > 255) { // Any other string field >255 (could explain why we missed it earlier). unknownFieldOverflows.push({ ticketId: rec.id, ticketNumber: rec.ticketNumber, field, length: v.length, limit: -1, preview: v.slice(0, 100) + '…', createDate: rec.createDate, }); } } } process.stdout.write( ` page ${pageIdx}: scanned ${items.length} (total ${totalScanned}, hits known=${violations.length} other>255=${unknownFieldOverflows.length})\n` ); if (violations.length >= MAX_VIOLATIONS) { console.log(`\nReached ${MAX_VIOLATIONS} known-field violations, stopping early.`); break; } url = data.pageDetails?.nextPageUrl || null; } console.log(''); console.log(`Scan ended. Pages: ${pageIdx}. Tickets scanned: ${totalScanned}.`); console.log(`Violations on KNOWN columns: ${violations.length}`); console.log(`Other string fields >255 chars: ${unknownFieldOverflows.length}`); console.log(''); if (violations.length === 0 && unknownFieldOverflows.length === 0) { console.log('NO violations found. The offending record may have been modified since the failed sync.'); process.exit(0); } if (violations.length > 0) { const byField: Record = {}; for (const v of violations) { const b = byField[v.field] || { count: 0, maxLen: 0 }; b.count += 1; b.maxLen = Math.max(b.maxLen, v.length); byField[v.field] = b; } console.log('Known-column violations by field:'); for (const [field, { count, maxLen }] of Object.entries(byField)) { console.log(` ${field.padEnd(24)} count=${count} maxLen=${maxLen} limit=${FIELD_LIMITS[field]}`); } console.log(''); console.log('First 10 violations:'); for (const v of violations.slice(0, 10)) { console.log( ` ticket ${v.ticketId} (${v.ticketNumber ?? '?'}) field=${v.field} length=${v.length} createDate=${v.createDate}` ); console.log(` preview: ${v.preview.replace(/\s+/g, ' ')}`); } } if (unknownFieldOverflows.length > 0) { console.log(''); console.log('Other (unmapped) string fields >255 chars — these would NOT cause the postgres error but worth noting:'); const byOther: Record = {}; for (const v of unknownFieldOverflows) byOther[v.field] = (byOther[v.field] || 0) + 1; for (const [f, c] of Object.entries(byOther)) console.log(` ${f.padEnd(28)} count=${c}`); } } main().catch((err) => { console.error('FATAL:', err); process.exit(1); });