can1357/oh-my-pi · error · ToolError

${label} must be an integer; got '${key}'

Error message

${label} must be an integer; got '${key}'

What it means

coerceIntegerKey converts a string key into a number/bigint for binding against an INTEGER primary key or rowid. If the key string is not a plain (optionally signed) decimal integer, it throws this ToolError with the given label (e.g. "Primary key 'abc'"). Very large integers fall back to BigInt, so only non-integer shapes fail.

Source

Thrown at packages/coding-agent/src/tools/sqlite-reader.ts:348

function getTableInfoRows(db: Database, table: string): SqliteTableInfoRow[] {
	getTableMasterRow(db, table);
	return db.prepare<SqliteTableInfoRow, []>(`PRAGMA table_info(${quoteSqliteIdentifier(table)})`).all();
}

function getTableColumns(db: Database, table: string): string[] {
	return getTableInfoRows(db, table).map(column => column.name);
}

function getPrimaryKeyColumns(db: Database, table: string): SqliteTableInfoRow[] {
	return getTableInfoRows(db, table)
		.filter(column => column.pk > 0)
		.sort((left, right) => left.pk - right.pk);
}

function coerceIntegerKey(key: string, label: string): number | bigint {
	const trimmed = key.trim();
	if (!/^-?\d+$/.test(trimmed)) {
		throw new ToolError(`${label} must be an integer; got '${key}'`);
	}

	const asNumber = Number.parseInt(trimmed, 10);
	if (Number.isSafeInteger(asNumber)) {
		return asNumber;
	}
	return BigInt(trimmed);
}

function coerceLookupValue(key: string, type: string): SqliteBinding {
	const normalizedType = type.trim().toUpperCase();
	if (normalizedType.includes("INT")) {
		return coerceIntegerKey(key, `Primary key '${key}'`);
	}
	if (normalizedType.includes("REAL") || normalizedType.includes("FLOA") || normalizedType.includes("DOUB")) {
		const parsed = Number(key);
		if (Number.isFinite(parsed)) {
			return parsed;

View on GitHub (pinned to 9690622007)

Solutions

  1. Pass a plain integer string (e.g. '42', '-7') for INT primary-key lookups, or use the rowid if the table lacks a suitable key.
  2. If the key is genuinely non-integer (UUID), the table's declared type likely shouldn't match INT — check PRAGMA table_info; the tool coerces based on declared type, so look up by the correct column.
  3. For composite primary keys, use the appropriate keyed-update path with each column bound separately rather than one joined string.
  4. Trim the key and validate with /^-?\d+$/ before calling.

Example fix

// before
await reader.row({ table: "users", key: "7f3c-uuid" }); // pk column is INTEGER
// after
await reader.row({ table: "users", key: "17" }); // use the integer pk/rowid
Defensive patterns

Strategy: validation

Validate before calling

function assertIntegerKey(key: string): number {
  const t = key.trim();
  if (!/^-?\d+$/.test(t)) throw new Error(`key must be an integer: ${key}`);
  return Number.parseInt(t, 10);
}

Type guard

function isIntegerKey(v: string): boolean { return /^-?\d+$/.test(v.trim()); }

Try / catch

try { return await reader.row({ table, key }); } catch (e) { if (e instanceof ToolError && e.message.includes("must be an integer")) { /* look up by rowid or re-fetch the correct pk */ } throw e; }

Prevention

When it happens

Trigger: Row lookup/keyed update with a non-integer key against an INT-typed primary key: UUID-style keys ('a1b2...'), composite keys ('1,2'), keys with whitespace-embedded junk, prefixed ids ('id:42'), or float strings ('1.0'). Called from coerceLookupValue, binding, and updateRowByRowId.

Common situations: Using a text primary key (UUID) against a table whose declared column type contains INT (note: SQLite type affinity matches on 'INT' substring); passing composite key parts joined into one string; copying a rowid that includes formatting.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/7b4423ac98ca8b6d. Report an issue: GitHub.