HarperFast / HarperFast/harper

@table directive changes are silently ignored on existing tables without a declared primary key

Open
#2,480 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 6h
Merged PRs (30d)
200

Description

## Summary

On an existing table whose GraphQL type declares no `@primaryKey` field, changing `@table` directive settings (`audit`, `expiration`, `eviction`, and likewise `sealed` / `replicate`) has no effect. The new values are silently dropped on every reload and restart, with no warning or log line. The same declaration is honored at table creation, so schema behavior is inconsistent between create and update.

## Mechanism

In `table()`, the only code that persists changed settings for an existing table sits inside the attributes loop under [`if (attribute.isPrimaryKey)`](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/resources/databases.ts#L2554), with the [audit comparison](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/resources/databases.ts#L2568) below it. A type with no `@primaryKey` field produces an attributes list containing no `isPrimaryKey` entry, and the graphql loader does not synthesize one ([graphql.ts uses `'id'` only as a display fallback in a log line](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/resources/graphql.ts#L384)). The settings comparison therefore never evaluates.

At creation the new-table branch does honor the directive ([audit resolution](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/resources/databases.ts#L2380), expiration/eviction adjacent), and such a table is created with an unnamed primary key (`primaryKeyAttribute = {}`). On boot every worker reads the persisted descriptor ([load path](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/resources/databases.ts#L1145)), so the creation-time value wins forever.

## Reproduction

1. Deploy a component schema and write a few records:

```graphql
type CacheEntry @table(table: "cache_entry", audit: false) {
value: String
}
```

2. Edit the directive to `audit: true`, restart.
3. `describe_table` still reports `"audit": false`, no audit records are produced, and replication (which rides the audit stream) never ships this table's writes.

Observed live on 5.2.7 across two full restarts (`describe_table` polled 12 times per restart, `false` every time). Mechanism confirmed against current main; the permalinks above are at 0db6cab.

## Sharp edges around the obvious workarounds

- Declaring a primary key after the fact does not help and is actively dangerous: with records present, the [primary-key-change guard](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/resources/databases.ts#L2305) throws mid schema load ("Cannot change the primary key ... because it already contains records"), and the declared name can never match an unnamed key. The throw lands during component loading.
- No operations API alternative exists: [`create_table` rejects existing tables](https://github.com/HarperFast/harper/blob/0db6cabe6bd04a6e6af8e774227d5a879848d561/dataLayer/schema.ts#L137) and there is no alter operation.
- `logging.auditLog` cannot override the descriptor's persisted explicit value (same load path as above).

A table in this shape therefore has no supported route to ever change these settings; the only recourse is a manual write to the internal catalog row.

## Suggested fix

Run the settings re-assert even when the declaration carries no primary-key attribute, either by hoisting it out of the `isPrimaryKey` iteration and addressing the `/` primary catalog row directly, or by synthesizing the primary-key attribute entry for PK-less declarations before the loop. While there, consider comparing `audit` / `sealed` / `replicate` against the persisted descriptor rather than the live `Table` values, matching the `schemaDefined` fix described in the comment just above the comparison, which addresses the same stale-disk-value class of bug.

Lavinia, via Claude

Contributor guide

Open the contributing guide

Research direction

Start in resources/databases.ts at table() and the existing-table settings comparison, then inspect the load path around line 1145 and GraphQL attribute handling in resources/graphql.ts. Reproduce the PK-less schema change and verify with describe_table after restart that audit, expiration, eviction, sealed, and replicate persist and take effect; use dataLayer/schema.ts to understand the existing-table API constraint.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, javascript, node.js
Domain
backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.