feat(spanner): add Instance, Database and BackupSchedule - #167
Merged
Conversation
Schemas and provisioners only; fixtures and conformance runs follow.
Instances are project-scoped - a Spanner instance's region is its config, not
a path segment. Two envelopes the config-driven engine cannot express are built
by a request transformer: create is {instanceId, instance:{...}}, with the id
beside the object rather than in a query parameter, and patch carries its field
mask in the body as fieldMask rather than in the query string.
A database has no name field on create; the id goes into a CREATE DATABASE
statement, quoted with backticks for GoogleSQL and double quotes for
PostgreSQL. dropDatabase is a plain DELETE on the resource URL, so the generic
engine handles it.
Backup schedules sit two collections deep and are synchronous. Spanner rejects
a wildcard for both nested collections - instances/-/databases and
databases/-/backupSchedules answer 400 "Invalid List... request" - so
databases and schedules are discovered by walking the collections above them.
All three cases are crud 8/8 and discovery 4/4 against europe-central2. Two failures found and fixed on the way, both real: displayName is rejected outside 4-30 characters, which the fixtures broke and the schema now states as a constraint rather than a comment. backupSchedules.patch accepts exactly three mask paths - encryption_config, retention_duration and spec.cron_spec.text - so UpdateMaskFromBody could not express it: the mask reaches a field inside spec, and that transformer lists the body's top-level fields. The mask is fixed instead, leaving out encryption_config so an update never rewrites a server value nobody declared. The nested cron fields GCP fills in (timeZone, creationWindow) are marked as provider defaults; without that a schedule reads back with two fields no forma declared and Verify fails. clean-environment.sh sweeps Spanner instances. Unlike most leftovers these are billed by the hour: the database and backup-schedule fixtures each build one as a prerequisite and conformance Destroy only removes the resource under test, so every run of those two cases leaves one running. Deleting the instance cascades its databases and schedules, so one sweep covers all three cases. ci.yml and nightly.yml both give spanner-* a 15 minute per-operation timeout. Running both phases back to back under one fixed FORMAE_TEST_RUN_ID, the way CI does, showed no name collision on the surviving instance, so no phase split.
Covers the two request envelopes, the DDL quoting per dialect, both directions of the config id normalisation, the native-ID parser at all three depths, and that the parent-walking List survives registration.
naxty
added a commit
that referenced
this pull request
Aug 31, 2026
…ustConfig Restores #171. That pull request was stacked, and it merged into its parent branch rather than into main - GitHub marks a stacked pull request merged once its commits reach its base, and its base was a branch. #167, #168, #169 and #170 went the same way but were recovered when #173 landed, because #173 descended from them. Nothing descended from #171, so certificate manager was the one batch left behind: main has carried a Merged badge and no code since. This is that content on top of current main, nothing else - the three types, their schemas, six fixtures, and the package registration. Verified locally against the service account CI uses, all three CRUD 7/7 with Replace skipped and discovery 4/4. Two things it needed, both recorded in the CHANGELOG: roles/certificatemanager.owner rather than editor, which carries no delete permission at all; and a trust config that is not empty, since the API rejects one carrying neither a trust store nor an allowlisted certificate.
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.
Adds Cloud Spanner. Stacked on #165 — review that first; this PR's own diff is
the three Spanner commits.
GCP::Spanner::InstanceGCP::Spanner::DatabaseGCP::Spanner::BackupScheduleVerified against
europe-central2on released formae (0.89.0) and on abinary built from formae
main, which additionally requires an extracted formato re-apply as a zero-operation simulate. All three green on both.
Notes for review
Three things the config-driven engine could not express, each handled in a
transformer rather than a bespoke provisioner:
{instanceId, instance:{…}}— the id sits in the bodybeside the object, not in a query parameter, so
RequestWrapper+CreateIDParamwould have put it in the wrong place.fieldMaskin the body, not?updateMask=, andrejects
configin the mask — so the mask is computed from a fixedmutable-field list.
CREATE DATABASEstatement, backtick-quoted for GoogleSQL and double-quoted for PostgreSQL.
configis normalised both ways — a forma writesregional-europe-central2andthe plugin qualifies it — so a forma carries no project id and stays portable
between targets.
backupSchedules.patchaccepts exactly three mask paths (encryption_config,retention_duration,spec.cron_spec.text), soUpdateMaskFromBodycannotexpress it: the mask reaches a field inside
spec. The mask is fixed instead,deliberately leaving
encryption_configout so an update never rewrites aserver value nobody declared.
Spanner fills in
spec.cronSpec.timeZoneandcreationWindow; both are markedhasProviderDefaultor Verify fails on properties no forma wrote.displayNameis constrained to 4–30 characters in the schema, because the APIrejects anything outside that and a fixture should fail at eval rather than
mid-apply.
Cost and cleanup
Instances are billed by the hour, and the database and backup-schedule fixtures
each build one as a prerequisite that conformance Destroy does not remove — so
clean-environment.shsweeps Spanner instances, verified against the liveproject.
spanner-*gets a 15-minute per-operation timeout inscripts/ci/run-conformance-case.sh.What I could not verify
Backupis deliberately absent:expireTimeis required, absolute and cappedat a year, so any fixture hardcodes a date that silently rots into a CI
failure.
FORMAE_TEST_RUN_IDshowedno name collision on the surviving instance, so no phase split was added.
Correction (2026-08-28): this body reported CRUD as
8/8. That was wrong. TheReplace step reports
[~]— skipped, not passed: the harness only runs it when atestdata/<case>-replace.pklfixture exists, and no case in this repo has one (0 of118). Read every
8/8above as 7 passed, Replace not exercised. The same appliesto every GCP resource already on
main— the Replace path has never been coveredhere. Nothing in this PR regressed.