Gotchas

These are the places where NCUA's data differs from what an engineer fluent in SQL would expect. Each entry gives the likely assumption, what the data actually holds, and a query that shows it. The queries are tested: when a trap stops being true, its test fails and the entry is removed.

1. A NULL has two meanings, and neither is zero

You would assume a NULL accounts.account_value means the credit union reported nothing, or zero. It means one of two things: published as text when accounts_text has the row, and not reported when it does not. The code does not tell you which. A blank arrives as NULL on any code, so the same code can be both:

-- A NULL account_value is text only when accounts_text has the row.
SELECT a.account_code,
       count(*) FILTER (WHERE t.account_text IS NOT NULL) AS published_as_text,
       count(*) FILTER (WHERE t.account_text IS NULL)     AS not_reported,
       count(DISTINCT t.account_text IS NULL) = 2 AS both_meanings
FROM read_parquet('https://data.quartercall.com/curated/accounts/quarter=2026Q1/data.parquet') a
LEFT JOIN read_parquet('https://data.quartercall.com/curated/accounts_text/quarter=2026Q1/data.parquet') t
  USING (cu_number, account_code)
WHERE a.account_value IS NULL
GROUP BY a.account_code
ORDER BY a.account_code;
examples/gotchas/null-has-two-meanings.sql

To tell the two apart, join to accounts_text: a NULL with a text row was published as text, and a NULL without one was not reported. A code the credit union did not file has no row at all, which also means it was not reported. None of the three is zero.

2. Not every profile field describes its quarter

You would assume a profile row describes the credit union as of its quarter. Most columns do. The name and profile_charter_number are what NCUA held when callbase fetched the filing: an old quarter's row carries today's name. And assets_prior_cycle is the previous quarter's total assets on every quarter measured but 2026Q2, the newest when this was measured, where it has been the quarter's own:

SELECT p.quarter, p.cu_number, p.profile_charter_number,
       p.assets_prior_cycle, a.account_value AS total_assets
FROM read_parquet([
  'https://data.quartercall.com/curated/profiles/quarter=2026Q1/data.parquet',
  'https://data.quartercall.com/curated/profiles/quarter=2026Q2/data.parquet']) p
JOIN read_parquet([
  'https://data.quartercall.com/curated/accounts/quarter=2026Q1/data.parquet',
  'https://data.quartercall.com/curated/accounts/quarter=2026Q2/data.parquet']) a
  USING (quarter, cu_number)
WHERE p.cuid = 5163 AND a.account_code = 'A010'
ORDER BY p.quarter;
examples/gotchas/profiles-mix-time.sql

NORTHERN's 2026Q1 row names charter 24989, which it took in 2026Q2. Its 2026Q1 assets_prior_cycle is not its 2026Q1 total; its 2026Q2 one is. For assets read A010 from accounts, always.

3. Not every filing has a profile row

You would assume every row in accounts has a profile row. A filing with no Profile block has none, and its accounts.cuid is NULL. Superseded filings have no profile row either: the table is keyed like accounts, one winning filing per credit union per quarter.

SELECT a.cu_number, a.cuid
FROM read_parquet('https://data.quartercall.com/curated/accounts/quarter=2026Q2/data.parquet') a
ANTI JOIN read_parquet('https://data.quartercall.com/curated/profiles/quarter=2026Q2/data.parquet') p
  USING (cu_number)
WHERE a.account_code = 'A010';
examples/gotchas/no-profile-row.sql

Join LEFT, and decide what a missing row means for your question. The manifest's profiles_missing counts them per quarter.

4. Many profile fields are text as filed

You would assume a date is a date and a flag is a boolean. NCUA declares many profile fields as strings, and they stay strings: dates as M/D/YYYY, flags as Yes and No, numbers in quotes.

SELECT typeof(is_low_income) AS type, is_low_income, count(*) AS credit_unions
FROM read_parquet('https://data.quartercall.com/curated/profiles/quarter=2026Q1/data.parquet')
GROUP BY ALL
ORDER BY is_low_income;
examples/gotchas/text-as-filed.sql

1/1/1900 in date_insured is NCUA's unknown. An empty string is published as NULL; nothing else is normalised. The profile fields reference gives each field's declared type.

5. The series begins at 2019Q1

You would assume older quarters are missing by accident. They are left out deliberately. callbase holds part of 2018Q3 and 2018Q4, which NCUA will not serve in full, and the manifest lists neither, so every quarter a script finds in the manifest can be read in full.

SELECT min(q.quarter) AS first_quarter,
       count(*) FILTER (WHERE q.quarter < '2019Q1') AS quarters_before_2019q1
FROM (SELECT unnest(quarters) AS q FROM read_json('https://data.quartercall.com/index/manifest.json'));
examples/gotchas/series-floor.sql

6. Count rows from the files, not the manifest

You would assume the manifest's submission_count is the number of credit unions in a quarter's file. It counts filings callbase holds, amendments and superseded filings included, and a quarter can be held before its files are built. cu_count counts institutions held.

WITH manifest AS (
  SELECT q.submission_count, q.cu_count
  FROM (SELECT unnest(quarters) AS q FROM read_json('https://data.quartercall.com/index/manifest.json'))
  WHERE q.quarter = '2026Q1'),
file AS (
  SELECT count(DISTINCT cu_number) AS credit_unions_in_file
  FROM read_parquet('https://data.quartercall.com/curated/accounts/quarter=2026Q1/data.parquet'))
SELECT submission_count, cu_count, credit_unions_in_file,
       submission_count > credit_unions_in_file AS filings_exceed_credit_unions
FROM manifest, file;
examples/gotchas/counts-from-files.sql

To learn how many rows a quarter's published file holds, count them in the file.

Contract schema 1.2 · docs built 2026-10-29 from platform 4894823