Reading a dbprint print
Generated by dbprint - do not edit by hand.
Vocabulary
Every column carries exactly one classification (SPEC 3):
boolean- Carries a fullvalueslist - the true/false split is exact over what was scanned (see scope, below), never a frequency sample.json- Carries a distinct-value count (cardinality) but novalueslist and nodistribution- the shape is unmeasured, only the count is.foreign_key_candidate- Carries a foreign key on this column, the referencing side - not the target.relationships.yaml's own entry saysdeclared(from the catalog),inferred(a naming guess a database will not enforce), ormeasured(proposed from value containment between two columns' sketches) - a measured edge is a stronger claim about the data at the instant of the read, never a stronger claim about the schema than an inferred one (SPEC 2.3); its value list follows the same truncation rule ascategorical/textbelow.categorical- A closed or sampled domain.values_coverage == 1.0licenses an exact-match predicate over what was scanned (see scope, below) - anything less is a frequent-value sample, not the whole set (SPEC 2.2.3).temporal- Percentiles here are always an actual observed value, never interpolated - every engine takes them by rank.freshness.max_age_daysclamps at0for a future-dated maximum (readslive, not negative) and is always0for a date-lessTIMEtype -range.maxcarries the true value regardless (SPEC 2.2.4).numeric- Percentiles may be interpolated (Postgres, Snowflake) - ap50is not guaranteed to be a value the column actually holds. MySQL always returns an observed value by rank.text- The value list may be exhaustive or a frequent-value sample, the same rule ascategorical- checkvalues_coveragebefore treating an absent value as absent from the column. A column flaggedlooks_like: prosecarries none of the three at all - the scan they need is one a producer skips on purpose.unsupported- Onlysql_type,nullable,null_count,null_rateandclassificationare measured (SPEC 3.3) - plusrows_scannedwhen the file'sscopeblock is present. No cardinality, no values - the producer declined to profile this type at all.
Residual traps
- A
p50is not always a value the column holds. Numeric percentiles interpolate on Postgres and Snowflake (PERCENTILE_CONT); temporal percentiles never interpolate, on any engine, because Snowflake cannot evaluate a continuous percentile against a timestamp ordering. MySQL takes every percentile by rank. - An inferred edge can resolve on a name coincidence, and a measured edge is not a stronger schema claim than either.
refers_to/referenced_byentries withdetection: inferredare a naming match, not a verified relationship; ameasuredentry is stronger evidence about the data atprofiled_at, never a stronger claim about the schema - a consumer MAY use either as a join candidate, never as cardinality-guaranteed, and SHOULD prefer adeclarededge over both where one exists (SPEC 2.3) -relationships.annotations.yamlrecords where a human has since rejected an inferred one. cardinalityis collation-relative. Two prints of one logical schema, taken through different engines or different column-level collations, can legitimately disagree on a text column's distinct count for this reason alone - it is not drift.approximatecan mean two different measurements.cardinality_method/row_count_method: approximatecovers both a live sketch this run computed and a catalog estimate of unknown staleness - the field alone does not say which (SPEC 2.2.2).- A measured
grain,dependenciesentry, ornull_patternscombination is an observation, never a constraint. Each states what held over the rows read atprofiled_at, on the same footing as an inferred relationship - not a rule the database enforces (SPEC 2.2.10, 2.2.12, 2.2.13). inferred.sensitivity's absence never means safe to publish. Nothing was detected - that is not a completeness claim, and this specification does not make one for the field either (SPEC 4.4.2).description.mdloses to the measured layer. On any questionstatistics.yamlanswers, prefer the statistic - the prose may describe a schema a later run already changed underneath it (SPEC 2.4).- A
catalog_onlyobject was never queried, not measured as empty. Its file carries the schema facts a catalog already knew and norow_countand no per-column measurement at all (SPEC 2.2.15). Read a statistic missing there as unasked - never as zero, and never as a value withheld. - A grain search that gave up ruled nothing out.
grain.search.exhausted: falsemeans a per-table cap cut the search short before it could test every candidate (SPEC 2.2.12) - the absence of a measured key is a gap in the search, not evidence that the table has none beyond those listed. - A declared artifact with no file on disk is not the same as one never declared. A manifest entry's
artifactsmap names every kind this table promised; a kind listed there whose file is absent is a broken promise the print SHOULD be treated as inconsistent for, not an absence licensed by the classification or object type (SPEC 2.5, 7.3). values_coverage_method: boundedmeans the coverage figure is a clamp, not a measurement. The value list and the population it is measured against were not read at the same instant, so an exhaustive-lookingvalues_coverage: 1.0underboundedis not the same claim as one with no hedge at all -measuredstates the two agreed,boundedstates a producer caught them disagreeing (SPEC 2.2.4).numeric/temporalcarryvaluesbut nevervalues_coverage;frequenciesis not an omission. The list is the same top-N fetchdistributionis computed from, but it is never exhaustive on these two classifications, so a validator has no exhaustive list to recomputedistributionfrom -frequencies's four counts -top,bottom,listed,total- are what it checks instead (SPEC 2.2.4). None of the four is a share; recompute any ratio againstnon_null/cardinalitybefore trusting a rounded one.unrepresentablechanges how a bound must be read, not just which fields are absent. A temporalmin/max/percentile outside the years 0001-9999 (proleptic Gregorian) is still emitted as text - the database's own rendering - but named here so a consumer feeding it to a typed parser degrades deliberately instead of crashing (SPEC 2.2.4). The marker says nothing about whether the value is correct.depends_on: []and the key omitted mean different things. A view or matview's[]states the catalog answered and it reads no other object in the print; the key omitted entirely states the producer could not ask - no grant, no such catalog table on this engine version, or the read failed for any other reason (SPEC 2.2.17). Collapsing the two into one[]would spend that meaning on every engine to cover one engine's own gap.- A field named in
unmeasuredwas attempted and lost, not forbidden. Every other absence a print carries is structural - the classification forbids the field, a redaction withheld it, the type has no day to truncate to - and SPEC 7 reads it that way. A name in a column'sunmeasuredlist (SPEC 2.2.4), or a block in the file's own (SPEC 2.2.1), states that this run issued the read and did not get an answer: treat that field as unknown, never as zero, none, or a property of the data. An artifact with no marker anywhere is not thereby complete - a producer that dropped a measurement silently looks identical. - A timeline gap is not a zero.
timeline.bucketslists only a day/week/month span containing at least one non-null anchor value - a span with none is absent from the list, never published as a zero-count entry, so two consecutive buckets whosestartvalues are not adjacent atunit's own width mark a gap where no row fell, not a measured absence of activity (SPEC 2.2.16).
Reading strategy
Start at manifest.yaml when reading a print straight off disk - it lists every table
and where its artifacts live, before opening any of them. An MCP client starts from
search_columns instead; the server names it as the entry point on connect. For a
broad question ("what does this warehouse track"), read manifests and DDL first;
statistics are large and most of a broad question is answered by table and column
names alone. For a narrow question about one table, ddl.sql and statistics.yaml
together usually answer it without a live query.
Stop reading and query the database when a question needs a value the print does not
publish - an exact row, a join across a predicate no column here encodes, anything
newer than profiled_at. The print is a snapshot; it does not replace the database. A
missing field is a different question first - SPEC 7 names what each absence can mean
before you read it as zero, none, or unmeasured.
A file carrying a top-level scope block did not read the whole table - a row
predicate narrowed it, or a sample bounded the cost. Every count in it except
row_count is over rows_scanned, not the table (SPEC 2.2.8) - a boolean's exact
split and a values_coverage: 1.0 are both exhaustive over that narrower set only,
never wider than what was actually read, and sum is not rescalable to table grain
by assuming the sample is representative: read it as a partial total, never the
column's true sum.
A physical_layout block declares a clustering, partitioning or sort key: mechanism
(cluster, partition or sort) names the mechanism, not a judgment; keys is ordered,
its first component pruning far more than its last; each key's column is what a
predicate matches against, expression what was actually declared. Absence means
the table declares none of the three, never that nobody checked - unless the
file's own unmeasured list names the block (SPEC 2.2.11, 2.2.1).
A column carrying a redacted marker (mask, drop, hash) withholds literals, not
measurements - cardinality, null_rate, values_coverage and distribution stay
true (SPEC 2.2.9). Do not order, compare, or do arithmetic on a bound from one: a
masked maximum still looks like a maximum, and a hashed bound sorts by digest, not
value. A redacted temporal column's max_age_days and range.span_days are floored
to the nearest 90 days, under every primitive including drop.
A table with no description.md has no human-authored context - grain, units and
exclusions are then whatever the DDL and statistics alone can support. Do not infer a
business rule the artifact does not state.
Signals nobody points at
diff.yaml is the latest structured diff only, overwritten every run (SPEC 1.2) - a
column carrying many change-kind entries this run is one whose statistics moved a lot,
not a history to read across prints. A column with no entries this run is not
necessarily stable: unevaluated_tables (SPEC 2.6.4) counts objects the diff had no
basis to compare at all - a plain view, or one this run did not re-read - and those
produce no events either.
referenced_by counts are a usage census. A table with a long referenced_by list is
load-bearing across the schema; one with none may be a leaf table, or may simply lie
outside every other table's selectors (SPEC 2.3.6) - eligible_target on the target and
the manifest's own selectors tell the two apart.
A column's sketch exists for a computation the producer deliberately does not run:
whether its distinct values overlap another column's, across tables or across prints,
with no second query against either database. dbprint.spec.sketch decodes it and
estimates that overlap; observed.containment/target_coverage are that same estimate
already computed wherever both endpoints of an edge sit in one print (SPEC 2.3.10),
alongside fanout_avg/fanout_max (average and worst-case rows per distinct
referencing key) and coherent (false when the child's cardinality exceeds the
parent's - arithmetically impossible for a real containment). answerable_count is
the denominator a containment ratio must be read against, not a headline number of
its own - the margin narrows as it grows, and a small one widens it sharply.
scope_compatible: false means the two endpoints could not be compared on equal
terms at all; every other field in the block is then absent, never zero, and no
ratio is published across a mismatched pair. A sketch below its own retained size is
exhaustive and answers single-value membership exactly; at or above it, membership
is not answerable at all.