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.composite— An array, record or map the producer looked inside: no value list of its own, but apartsmap keyed by path ([*],.sku,[keys]), each part a column-shaped block over its ownoccurrences.parts_foundabove the number listed means the rest were cut or withheld;sizeis how many parts each value holds (SPEC 2.2.18).spatial— Geometries, described but never counted or listed: nocardinality, novalues.geometrygives the kinds, reference systems (srids) and coordinate dimensions with their counts, plus the empty and invalid counts;extentis the X/Y bounding box in the engine's own axis order, rounded outward. Every spatial column carriesinferred.sensitivity: geolocation, and aredactedmarker withholdsextentalone (SPEC 2.2.4, 2.2.9).vector— Embeddings, never counted or listed: nocardinality, novalues, noredactedmarker.dimensionis the element count a query embedding must match (min < maxmeans the column mixes dimensions),normthe Euclidean norm bounds of the non-zero vectors (both near 1: unit-normalized, so inner product ranks as cosine), andzero_countthe zero vectors cosine distance is undefined on (SPEC 2.2.4).foreign_key_candidate— Carries a foreign key on this column, the referencing side — not the target. The edge'sdetectioninrelationships.yamlisdeclared(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.0means an exact-match predicate is complete 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 a value the column holds, never interpolated — every engine takes them at the nearest rank, or near it where its construct is approximate (SPEC 2.2.4).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 interpolate on every engine but MySQL, which takes them by rank, and BigQuery, which approximates them — ap50is not guaranteed to be a value the column actually holds.binary— Bytes, counted but never listed:cardinality,inferred.candidate_key, alengthin bytes andempty_count(zero-length values), novalues. A binary foreign key or a low-cardinality binary column lists its values as lowercase hex with no prefix (0aff), the spelling every engine's binary literal accepts (SPEC 2.2.4).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 producer does not read the values of a prose column.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.
Fields that are easy to misread
- A table missing from
tablesmay still exist. The manifest'sfailed_tablesnames every table the last run attempted and could not profile; one named there and absent fromtablesexists and is unprofiled, and one named there and present is an earlier run's print, dated by its ownprofiled_at(SPEC 2.5). - A
p50is not always a value the column holds. Numeric percentiles interpolate on every engine but MySQL and BigQuery; temporal percentiles never interpolate, on any engine (SPEC 2.2.4). 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, and where a human has added an edge the producer could not emit (SPEC 2.7.2): a reader carries that one taggedannotated, a human's word with nothing measured behind it. 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 does not distinguish the two (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. It means nothing was detected; the detector does not find every sensitive column, and this specification does not require it to (SPEC 4.4.2).- Where
description.mdandstatistics.yamldisagree, usestatistics.yaml. The description is written by hand and may describe the table as it was before a later run recorded a schema change (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). A statistic missing there was never requested — read it as neither zero nor a value withheld. - An
externalobject's rows live in another system. Every query against it reads that system, whatever its statistics say (SPEC 2.2.20). Besidecatalog_onlynothing was read through it; without it the statistics describe rows fetched from the other side. grain.search.exhausted: falsedoes not rule out a key. It means 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 artifact kind this table declares; a kind listed there whose file is absent makes the print inconsistent, and it SHOULD be treated as such — the classification or object type does not allow that absence (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 avalues_coverage: 1.0underboundeddoes not state that the list is exhaustive —measuredmeans the two agreed,boundedmeans the producer found them disagreeing and clamped the figure (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 does not state whether the value is correct.depends_on: []and the key omitted mean different things. A view or matview's[]means the catalog was read and the object reads no other object in the print; the key omitted entirely means the dependency read did not happen — no grant, no such catalog table on this engine version, or the read failed for any other reason (SPEC 2.2.17). A producer never writes[]for the second case, so[]keeps one meaning on every engine.- A field named in
unmeasuredwas not measured this run; nothing forbids it. 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 lists each. 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 the read failed: 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 measurement named in
disabledwas switched off, not measured. The file'sdisabledlist names what configuration turned off for this table —looks_like,null_patterns,parts,relationships.infer,sketches,timeline— and every field or block such a measurement produces is absent for that reason alone. Read each as unknown, never as none: an absentnull_patternsthere says nothing about nulls, and an absentlooks_likeis not "no pattern" (SPEC 2.2.21). - 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 span with no rows, not a measured zero (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 calls the server's tools instead; the server's instructions name the tool for each question. 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. Before reading a missing field as zero, none, or unmeasured, check what its absence means: SPEC 7 names what each absence can mean.
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 the block was not read — unless the file's own unmeasured list names the block (SPEC 2.2.11, 2.2.1).
A merging block means the table's ClickHouse engine combines rows sharing its sorting key only when it merges parts, so row_count, cardinality and grain count the stored rows, which may repeat a key: query with FINAL (or GROUP BY the key, where one_row_per_key is true) for the logical rows. rows: merged says the statistics themselves were read with FINAL (SPEC 2.2.19).
A column carrying a redacted marker (mask, drop, hash) publishes only its counts — cardinality, null_rate, values_coverage, distribution and each value's count stay true, and every statistic computed from what the values are (mean, sum, length, the degenerate-value counts) is withheld (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.
Writing a query against a printed table
For each table the query touches, read ddl.sql; the refers_to and referenced_by edges in relationships.yaml, which are the join paths — each marked declared, inferred or measured (SPEC 2.3): an inferred edge is a guess from a column name, a measured one a value containment seen at the read, neither a constraint, so prefer a declared edge where one exists; each column's values with its counts and values_coverage; the column notes in statistics.annotations.yaml; and the notes in manifest.annotations.yaml at the print root (SPEC 2.7.3), where a fact many tables share — what a code means, which unit a timestamp is in — is written once. Leave the rest of statistics.yaml — counts, ratios, percentiles, distributions — unread: they describe the data, not what a predicate needs.
Write a literal in a listed value's exact spelling. A list at values_coverage 1.0 is the whole column — over the rows scanned where the file carries scope; below it, or on a numeric/temporal column whose frequencies.listed is short of cardinality, a phrase absent from the list is not evidence it is absent from the column. An entry carrying spelling_of is another spelling of the value it names (SPEC 2.2.4) — one category stored several ways, so a predicate needs every spelling in the group.
Every number in a print is written positionally, never in exponent form (SPEC 2.2.6), so paste it into SQL as it stands. A null_rate or coverage below 1.0 means some rows are not covered, however close it is: a column that is 99.96% null still holds values.
With dbprint installed, dbprint context <table> --purpose query renders exactly this selection off the print, join paths included, with no server running. Served over MCP, get_table_context with purpose: query is the same selection, and resolve_value returns how a phrase is spelled in one column; the server's instructions say when to call it.
The diff, reference lists and sketches
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. Nor is a 0 in columns_default_changed, indexes_changed or comments_changed evidence that nothing moved: column defaults, indexes and comments are recorded only in ddl.sql, which the diff does not read (SPEC 2.6.10), so those three counters are always 0.
referenced_by lists the tables whose columns reference this one. A table with a long list is referenced from many places in the schema; one with none may be a leaf table, or may 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 target_table, referencer_table or depends_on entry absent from manifest.tables names an object outside the print. Read it as an opaque name: its periods are not a schema/table boundary you can split on, and no directory in the print stands for it (SPEC 1.3).
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 result on its own: the ratio's error shrinks as it grows and is large when it is small. 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 tests single-value membership exactly; at or above it, membership cannot be tested.