---
title: "dbt models that never say what one row is | Metacenta"
description: "\"How many customers?\" is a row count only if one row is one customer. Where dbt lets you declare a model's grain, and which declarations count."
url: "https://metacenta.com/rules/core-model-grain-undeclared"
updated: "2026-10-14"
tokens: 753
---

Modelling correctness · rule core-model-grain-undeclared

# dbt models that never say what one row is

A Metacenta review checks this under the rule **Core models declare their grain**. Everything below applies whether or not you ever commission one.

## What this rule checks

This rule flags each core model whose grain is declared nowhere. A model counts as core when a dbt exposure, Looker explore or Tableau view depends on it. If none is declared, the marts count instead. Any one of these counts as declared: a uniqueness test, even a filtered one; an incremental `unique_key`; a `primary_key` or `unique` constraint; or a primary or unique semantic entity.

## Why it matters

A count or a join assumes one row per key. If the table holds one row per key per day, an agent's totals are multiplied, and no query fails to warn anyone.

## How to fix it

**Declare what one row of each main table is.** Declare the grain. Add a `unique` test on the key, or `unique_combination_of_columns` for a compound key. Under a contract, a `primary_key` constraint states it too.

Before:

```yaml
  - name: dim_customers
    columns:
      - name: customer_id
```

After:

```yaml
  - name: dim_customers
    description: One row per customer.
    columns:
      - name: customer_id
        tests:
          - unique
          - not_null
```

## When it is fine to leave

An event log whose rows are genuinely not unique on any key can go without, if its description says so. Where the testing rules already ask for a uniqueness test, one test fixes both, and the review lists the work once.

## What we need to check it

`manifest.json`. Consumers come from exposures in the manifest, or from LookML or Tableau files if you share them.

## Published rules it corresponds to

**[dbt_project_evaluator](https://dbt-labs.github.io/dbt-project-evaluator/)**, rule `fct_missing_primary_key_tests`. Ours checks a subset of what it flags; theirs is stricter. dbt Labs' rule asks every model for a primary-key test. This one asks only the models behind a consumer (or the marts), and also accepts a `unique_key`, a key constraint or a primary semantic entity, so theirs flags strictly more.

This means our check corresponds to their rule. It does not mean the publisher reviewed or endorses it.

[Ask about a review](https://metacenta.com/enquire?src=rule-core-model-grain-undeclared&via=md)

## Related pages

- [dbt incremental models: how unique_key works](https://metacenta.com/rules/incremental-without-unique-key)
- [dbt public models with no enforced contract](https://metacenta.com/rules/public-model-without-contract)
- [dbt sources declared twice for one table](https://metacenta.com/rules/duplicate-source)
- [dbt staging models: how to set up the staging layer](https://metacenta.com/rules/staging-depends-on-staging)
- [This rule in a sample review](https://metacenta.com/sample#constructed)
- [All published rules](https://metacenta.com/rules)
- [How a review is assessed](https://metacenta.com/standard)
