Repository navigation
db, tests, ci: the foundation schema in one file, proved on every push (foundation m0) - #275
Merged
Merged
Conversation
…h (foundation m0) database/schema.sql builds the whole foundation schema of the foundation plan v5 Part 1 on an empty PostgreSQL 18 in one psql -f: the scientific tables with a not-null job on every row and a plain per-job unique scientific key, runs, jobs, job_inputs, official with partial unique indexes on open bindings, one _official view per product table, members_official over the predecessor chain minus excluded source sets, and the q3c extension and indexes inside a block that notices and continues where q3c is absent. foundation.yml applies it and runs tests/db/test_schema.py; db-migrations.yml ignores that module. tests/db fails rather than skips when PGHOST is unset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
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.
What and why
Milestone 0, task 1 of the foundation plan (PLAN.md v5, 2026-10-20, held outside the repository until its rapid_docs page lands after milestone 3): the one schema file of Part 1, applied to an empty PostgreSQL 18 by
psql -v ON_ERROR_STOP=1 -f database/schema.sql, a CI workflow that proves it on every push, andtests/dbfailing rather than skipping without a database. No code reads the new schema yet; task 2 adds the publisher and the change operation against it.Files
database/schema.sql(new): static tables with dev's data inline,runs,jobs,job_inputs,official, the scientific tables, the_officialviews,members_official, the q3c block.database/README.md: the apply command, once..github/workflows/foundation.yml(new): copied in shape fromdb-migrations.yml(postgres:18 service, Q3C install, client, emptyrapiddatabase), then the apply, thenpython -m pytest tests/db/test_schema.py -q -rs..github/workflows/db-migrations.yml: its pytest step gets--ignore=tests/db/test_schema.py.cli-behaviour.ymlis untouched (it runstests/cli). Task 3 extends the exclusion totests/db/proof/,test_publish.pyandtest_change.py; task 4 deletes the old workflow.tests/db/conftest.py:connfails with "PGHOST is not set; database tests need a database".tests/db/test_connection_live.py: its own PGHOST skip becomes the same failure, so no test intests/dbskips for want of a database.tests/db/test_schema.py(new): the schema applies to a fresh empty database (its own scratch database, created and dropped); the plan's tables and views exist; the partial unique index refuses a second open binding on one key and allows one after the first closes; a product row with a null job is refused onjob;members_officialreturns a three-set chain's memberships and omits a source set named in anexcludesrow, whose rows still exist; underCI=trueq3c is present (skipped with that reason otherwise).Names task 2 codes against
Keys.
official.keyandjob_inputs.keyare jsonb objects whose names are the table's column names; the views build them withjsonb_build_object, so the publisher must build the same object:official.product)exposuresdataset, expidexposuresl2filesdataset, expid, sca, fidl2filesdiffimagesdataset, expid, sca, fiddiffimagespsfsdataset, fid, scapsfsrefimagesdataset, field, fidrefimages; childrenrefimimages,refimcatalogssources(source set)dataset, expid, sca, fidsourcesobjects(object set)dataset, survey, fieldobjects; childmembershipspackets(packet set)dataset, expid, sca, fidpackets; childrenforced,externalcatalogsname, versioncatalogsProduct views join open
officialrows on (product, key, job). A set is identified by its job, so the set-child views (refimimages,refimcatalogs,sources,memberships,packets,forced,external) join through the set's open binding on (product, job): a set job must bind at most one key of its set product; the publisher should hold to that.Bookkeeping.
runs (id, who, created, why, revision, settings jsonb, survey text, dataset text), id an identity.jobs (id, run, stage, scope jsonb, state, image_digest, settings_hash, published_attempt, manifest jsonb, refusal, launched, started, ended, exit_code).stateis one oflaunched(default),running,published,failed; a CHECK tiesstate = 'published'topublished_attempt IS NOT NULL, so the publisher sets both in one update.refusal(text) is where the publisher records a refused automatic binding; the brief needed it on the job row and named no column.job_inputs (job, kind, product, key jsonb, source_job, binding),kindininput,expects,excludes. An object-set job's predecessor is itsinputrow of productobjects(source_job= the predecessor's job);members_officialfollows exactly that.expects:binding= the open binding id the job expects to replace, null for none.excludes: productsources, the source set's key,source_job= that source set's job.official (id, product, key, job, run, survey, dataset, opened_group, opened_by, opened_at, opened_why, closed_group, closed_by, closed_at, closed_why). A product binding has product, key, job and no run; the live binding has run, survey, dataset and no product, key or job (CHECK). A second CHECK makes the four closed_* fields all null or all set. Partial unique indexes:official_open_product_key (product, key) WHERE closed_at IS NULL AND job IS NOT NULL,official_open_live (survey, dataset) WHERE closed_at IS NULL AND run IS NOT NULL.opened_atdefaults to now().change_groups: a bigint sequence for group ids (nextval('change_groups')), the smallest way to give each change group one new id.Views. Each
<table>_officialist.*plusbinding(the openofficial.id) andrun(fromjobs).members_officialismemberships.*plusset_job(the official object set the row belongs to),bindingandrun;jobis the set in the chain that recorded the membership.Scientific tables. Every row has
job bigint NOT NULL REFERENCES jobs. Image row ids (rid,pid,rfid,psfid,rfcatid) areinteger GENERATED BY DEFAULT AS IDENTITY, so the publisher may omit them or supply them. A source is(job, sid)withsid bigintassigned by the producer; an object is(job, aid)withaid bigintthe position hash; a membership is(job, aid, source_job, sid); a packet is(job, source_job, sid); a forced row(job, forced_id); an external match(job, source_job, sid, catalog_job, catid).Column choices that are mine
l2filemetafolded intol2files(centrera0, dec0and cornersra1..dec4; itsrid, fid, sca, hp6, hp9are already there): one row per image, one binding, one view.refimmetafolded intorefimages(nframes, cov5percent, the two columns the team reads), nullable because not every reference recipe computes them.dataset text,survey text(a survey such as a program title spans severalprogram_idvalues;surveysstays dev's table keyed byprogram_id, and nothing references it yet).expidis the exposure's scientific id from its producer, no sequence (joins are on scientific keys).diffimages.rid,rfid,refimimages.rfid, rid,refimcatalogs.rfid,sources.pid,forced.rid, pid: kept as dev's lineage columns with no foreign keys; joins between products go by scientific key (Part 1).checksumkept on every file-bearing table where dev had it (immutable bytes).objects:aid, dataset, survey, field, ra0, dec0(founding position) and the sumsnsources, sumra, sumra2, sumdec, sumdec2, sumflux, sumflux2; dev's mean and stdev are derived from them, not stored.memberships (job, aid, source_job, sid);packets (job, dataset, expid, sca, fid, object_job, aid, source_job, sid, trigger_mjd, band, schema_version, filename, byte_offset, byte_length):offsetandlengthrenamed to avoid the SQL keyword.forced: dev'sFORCED_TABLE_DTYPEcolumns (forced_id, aid, rid, pid, expid, sca, fid, band, mjdobs, ra, dec, x, y, psf_flux, psf_fluxerr, science_flux, science_fluxerr, flags, time_proc).external: the match envelope every dev match record opens with (catid, ra, dec, sep, pa) plussource_job, sid, catalog_job; Gaia's other fields come with the packet-stage port.catalogs (job, name, version, location): no dataset column, since its key is (name, version) per the brief.schema.sql(11 rows and a generate_series), so there is no second data file.jobindexes, the view-join indexofficial (product, job) WHERE closed_at IS NULL, the group indexes undo reads, the GIN index onoverlapfields, and dev's field/mjd/jd indexes on the image tables. No grants or roles: SMDC's grants arrive with milestone 1.Dev columns dropped, with the reason
vbest,version,ppideverywhere: the binding, the job and the run replace them (Part 1).l2files:version, vbestas above; dev's other header columns (ctype*, cunit*, the SIPa_*/b_*terms,equinox, paobsy, pafpa, skymean) andl2filemeta'sx, y, z, mjdobs: no team query reads them (LEDGER-queries.md(d)).exposures:field, hp6, hp9, fid, mjdobs, status, infobits, createdand the ASDF observation columns: not read by any query.refimages:hp6, hp9, svid, avid, archivestatus;refimmetaexceptnframes, cov5percent;refimcatalogs:ppid, field, hp6, hp9, fid, svid, status, created, archivestatus, avid(its reference key is its reference's job).diffimages:hp6, hp9, svid, avid, archivestatus, nalertpackets.psfs:version, vbest.sources:npix, rb, nneg, nbad, sumrat, rblabel(stored but never read by name; rubrat and sumrat are left out until breadth, Part 2).diffimmeta,alertnames,socprocs,procreqs,archiveversions, dev'sjobs: not in Part 1;jobsis the new one.Divergences from PLAN.md, with the reason
pipelinesandswversionsare not carried: the run's revision and settings replace them; a divergence from the plan's static-table list, made on purpose per the brief.skytiles,vskytiles,decbinsare not created. The repository disagrees with the plan on a fact here, and the repository wins. Neither the branch's baseline migration nor dev'srapidOpsTables.sqlcreates them in PostgreSQL: dev kept them in a SQLite file (database/schema/roman_tessellation_nside512.txton dev), and the branch computes the tessellation in closed form with no database access (database/modules/utils/roman_tessellation.py,RomanTessellationClosedForm;database/README.mdsays so). Loading 6,291,458 generated rows would need a load script, which this task forbids. Empty tables that nothing fills or reads would only cost something. If a later milestone wants them in SQL, they are oneCOPYfromgen_rows()andgen_decbins().l2files,diffimages,objects,fieldsonra0, dec0), not corners and notrefimages: every team q3c query addressesra0, dec0(q3c_radial_query(ra0, dec0, ...),q3c_dist(ra0, dec0, ...)in dev'srapid_db.pyandalerts/), and q3c indexes points, so corner indexes would not be used. Dev'srefimageshas no position columns, so there is nothing to index there. The block catches onlyfeature_not_supportedandundefined_file(q3c absent), so any other error in it still fails the apply.Measurements
tests/db/test_schema.pylocally: 5 passed, 1 skipped (the q3c test, not CI). WithPGHOSTunset: 5 errors "PGHOST is not set; database tests need a database", 1 skipped (the q3c test's own skip, which is about the environment and not about a missing database).tests/unit: 1735 passed.tests/dbmodules need the migration stream, whose baseline needs q3c, so they run only indb-migrations.yml.Left for the next task
publish,change,undo,create_run,create_job,run_show,job_showagainst the names above; group ids fromnextval('change_groups'); record a binding refusal injobs.refusal; setstateandpublished_attempttogether; addtest_publish.pyandtest_change.pytofoundation.ymland todb-migrations.yml's--ignorelist.tests/cli/conftest.pystill skips withoutPGHOST; it goes withtests/cliin task 4.🤖 Generated with Claude Code