fix: version history integrity — every model change restorable, write guards, ClickHouse String cast - #87
Merged
Conversation
Cube 1.7.30 ClickHouseQuery.sqlTemplates() never overrides templates.types.string, so the native planner CASTs multi-column primary keys for count measures without sql to the base 'STRING', which ClickHouse rejects (Unknown data type family: STRING). Extend the version-pinned postinstall patch to set it to 'String'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- versions.source_version_id (nullable FK, ON DELETE SET NULL) records
which version a rollback restored; insertable/selectable by role user.
- dataschemas update/insert (role user) only touch rows of the current
version, so history can no longer be rewritten in place. Nested
insert_versions_one { dataschemas } saves still pass (verified).
- versions insert requires owner/admin of the team (was: any member or
the datasource owner, allowing empty versions to become current).
- audit_logs.action also allows 'dataschema_update' (PUT dataschema).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract commitVersionFiles() from rollbackVersion(): one helper that writes a new version holding exactly the given files with the caller's Hasura token, returns the new rows, and drops the caller's cached user scope so its next request resolves the new version. Rollback now sets source_version_id = toVersionId, and the rollback audit payload carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DELETE /api/v1/dataschema/:id deleted the row from the current version in place, destroying history. It now writes a new version on the branch with every other file of the current version, so the delete is undoable with a rollback. Guards are unchanged (auth, partition, owner/admin, current + active, reference scan/409) and moved to utils/mutableDataschema.js for reuse. The success audit row is written directly (the delete event trigger no longer fires); the response adds versionId + branchId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Body {code}. Copies every file of the dataschema's version into a new
version with this file's code replaced, so the previous version stays
restorable — replaces the in-place Hasura update_dataschemas_by_pk the
agent refinement script uses. Same guards as DELETE (direct-verify,
partition, owner/admin, current version of an active branch → else 409).
Identical code returns 200 {unchanged: true} and writes nothing.
Response: {versionId, branchId, dataschema: {id (new row), name,
checksum, version_id}}. Audits under action dataschema_update and emits
Model Saved like createDataSchema. New error codes
update_blocked_authorization / update_blocked_historical_version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/v1/internal/invalidate-cache had no auth and is reachable via the /api ingress. Accept it only when x-hasura-admin-secret equals HASURA_GRAPHQL_ADMIN_SECRET (constant-time compare; 401 otherwise, and always 401 when the env var is unset). Its only caller, the actions service (utils/cubeCache.js), now sends that header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The diff only flagged a member as modified when its type changed; sql, title, description, format, meta, primary_key, public, segments, joins, pre_aggregations and cube-level sql/sql_table/refresh_key/extends changes were invisible, and views were ignored. Cubes and views are now compared attribute by attribute (named lists member by member). Additive response fields: modifiedCubes[].changedAttributes (e.g. sql_table, measures.revenue.sql, joins.customers), kind (cube|view) on every entry, and modifiedFiles[] listing every file whose code differs. A cube added to an existing file is reported in addedCubes. diffModels' smart-generate preview semantics are unchanged; parseCubesFromJs can optionally collect view() definitions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate-models and non-dry-run smart-generate write versions with the admin secret and trusted body.branchId, so any team member could write models, and a caller could write onto another team's branch. Both now require the branch to belong to the request's datasource and — except smart-generate dry runs — the caller to be owner/admin of its team. The row-type pipeline identity is provisioned as admin, so it keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- commitVersionFiles() is the only server-side version insert: PUT,
DELETE, rollback and createDataSchema (now a thin adapter keeping its
throw-on-error / {id} contract and caller checksum) all go through it,
and it is the single place that emits Model Saved. The returning-less
upsertVersionMutation twin is gone.
- utils/modelWriteGuards.js (was mutableDataschema.js): authorizeTeamWrite()
= partition + owner/admin gate with failure audit, shared by
resolveMutableDataschema (PUT/DELETE) and version rollback.
- hasuraTokenForUser() in mintHasuraToken.js replaces four copies of the
cached get-or-mint (delete/put, rollback, validate-in-branch, hasura
proxy); respondError() lives in errorCodes.js instead of per-route copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/v1/version/diff read both versions with the admin secret and
only checked the partition claim, so a caller whose token carries no
partition (Hasura HS256, WorkOS without partition) could diff any team's
versions. It now requires the branch to be visible to the caller through
the same guard the generation routes use — authorizeModelWrite renamed
to authorizeBranchAccess({readOnly}) — and answers 404 otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Debian 11 LTS ended 2026-08-31. deb.debian.org purged the bullseye-security pool (between Sep 3 and 5) but still serves its index, so apt-get install libssl1.1 resolves to 1.1.1w-0+deb11u8 and 404s on every arch; archive.debian.org has no bullseye-security yet. Read that suite from snapshot.debian.org's frozen final state (20260903T000000Z, index dated 2026-08-31) with check-valid-until=no, keeping the same final security versions the 2026-09-03 images shipped. Stop-gap: the base gets no further security updates; moving to a bookworm base is the real fix. Verified: buildx linux/amd64 and linux/arm64 builds pass, CI's in-image yarn test is 695/695 on both, amd64 image serves /livez on the vi stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Recovering Blue Car's deleted
vehicle_telematics_pointson the dev cluster showed that model history could not be trusted: several write paths changed the current version in place, so earlier content was lost for good, and history said nothing about restores. Details + evidence:synmetrix-vi-evidence.md(isolated stack), summarised below.What
countover a multi-column primary key compiled toCAST(x AS STRING); ClickHouse rejects it (Unknown data type family: STRING), breaking every such cube on dev (e.g.booking_forecast_data_points). Postinstall patch setstemplates.types.string = 'String'(pinned to 1.7.30, refuses to apply to a different source).DELETE /api/v1/dataschema/:idwrites a NEW version without the file (was: delete the row in place).PUT /api/v1/dataschema/:id{code} saves one file as a new version (same guards as DELETE;unchanged: trueon identical code; 409 unless current version of an active branch). Replaces the agent's in-placeupdate_dataschemas_by_pkrefinement save.versions.source_version_id, carried in the rollback audit payload./internal/invalidate-cacherequires the admin secret (was unauthenticated and reachable through the ingress); actions sends it.body.branchId.commitVersionFilesis the only version insert (PUT, DELETE, rollback, createDataSchema); shared guards inmodelWriteGuards.js;hasuraTokenForUserreplaces 4 copies.Verification
cubejs 695/695 (+
test/91/91), actions 53/53,lint-error-codes✅; image builds amd64 + arm64 and passes CI's in-image test. Every fix exercised live on an isolated compose stack (real Hasura/Postgres/cubejs/actions/ClickHouse).Deploy
synmetrix-{cube,actions,hasura-migrations}.PUT /dataschema/:id.🤖 Generated with Claude Code