can1357/oh-my-pi · error · ToolError

SQLite table '${table}' has a composite primary key; use '?w

Error message

SQLite table '${table}' has a composite primary key; use '?where=' instead

What it means

resolveTableRowLookup determines how a single row can be addressed (`table:key`). A single-column primary key maps to `kind: pk`; when the table declares a composite (multi-column) primary key, no single value can identify a row, so the tool throws and points at the `?where=` selector instead.

Source

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

export function getTablePrimaryKey(db: Database, table: string): { column: string; type: string } | null {
	const primaryKeyColumns = getPrimaryKeyColumns(db, table);
	if (primaryKeyColumns.length !== 1) {
		return null;
	}

	const column = primaryKeyColumns[0]!;
	return { column: column.name, type: column.type };
}

export function resolveTableRowLookup(db: Database, table: string): SqliteRowLookup {
	const primaryKeyColumns = getPrimaryKeyColumns(db, table);
	if (primaryKeyColumns.length === 1) {
		const column = primaryKeyColumns[0]!;
		return { kind: "pk", column: column.name, type: column.type };
	}
	if (primaryKeyColumns.length > 1) {
		throw new ToolError(`SQLite table '${table}' has a composite primary key; use '?where=' instead`);
	}

	const schema = getTableSchema(db, table);
	if (/\bWITHOUT\s+ROWID\b/i.test(schema)) {
		throw new ToolError(`SQLite table '${table}' does not expose ROWID; use '?where=' instead`);
	}

	return { kind: "rowid" };
}

export function queryRows(
	db: Database,
	table: string,
	opts: { limit: number; offset: number; order?: string; where?: string },
): { columns: string[]; rows: Record<string, unknown>[]; totalCount: number } {
	const columns = getTableColumns(db, table);
	const validatedWhere = validateWhereClause(opts.where);
	const whereClause = validatedWhere ? ` WHERE ${validatedWhere}` : "";

View on GitHub (pinned to 9690622007)

Solutions

  1. Use the query selector with a where clause: `db.sqlite:join_table?where=user_id=1 AND role_id=2`
  2. Use a raw SQL query: `db.sqlite?q=SELECT * FROM join_table WHERE user_id=1 AND role_id=2`
  3. If single-row addressing is needed, alter the schema to use a single-column primary key or a UNIQUE single column

Example fix

// before
sqlite://app.db?user_roles:5:9
// after
sqlite://app.db?user_roles?where=user_id=5 AND role_id=9
Defensive patterns

Strategy: validation

Validate before calling

const pkCols = db
  .query(`PRAGMA table_info(${quoteSqliteIdentifier(table)})`)
  .all()
  .filter(c => c.pk);
if (pkCols.length > 1) {
  // composite key: use where= selector instead of table:key
}

Try / catch

try {
  const lookup = resolveTableRowLookup(db, table);
} catch (err) {
  if (err instanceof ToolError && err.message.includes("composite primary key")) {
    // route caller to ?where=col1=v1 AND col2=v2
  } else throw err;
}

Prevention

When it happens

Trigger: Calling resolveTableRowLookup (indirectly via a `db.sqlite:table:key` selector) against a table whose CREATE TABLE has PRIMARY KEY (a, b) or multiple column-level PRIMARY KEYs.

Common situations: Join tables / many-to-many mapping tables (user_id, role_id); ORM-generated schemas with composite keys; trying row lookup on a table that was designed to be addressed by a tuple.

Related errors


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