@pgsage/loaders 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @pgsage/loaders
2
+
3
+ Data ingestion pipelines for pgsage. Currently ships the Census ACS 5-year estimates loader.
4
+
5
+ ---
6
+
7
+ ## Running
8
+
9
+ ```sh
10
+ # Full load — all 50 states + DC + PR (~3–5 minutes)
11
+ pnpm --filter @pgsage/loaders load:census
12
+
13
+ # Smoke test — Rhode Island only (~10 seconds)
14
+ pnpm --filter @pgsage/loaders load:census:smoke
15
+ ```
16
+
17
+ Requires `DATABASE_URL`, `VOYAGE_API_KEY`, and optionally `CENSUS_API_KEY` in `.env`.
18
+
19
+ ---
20
+
21
+ ## What Gets Loaded
22
+
23
+ The Census ACS loader fetches from the [Census Bureau API](https://api.census.gov/data.html) and populates three tables:
24
+
25
+ | Table | Rows | Description |
26
+ |-------|------|-------------|
27
+ | `public.geography` | 3,274 | States + counties with FIPS geo IDs |
28
+ | `public.variables` | 247 | ACS variable codes, labels, and categories |
29
+ | `public.estimates` | ~808,678 | Estimate values + margins of error |
30
+
31
+ After loading, the schema is indexed into `public.schema_embeddings` using Voyage AI `voyage-4-lite` embeddings (1024 dimensions) for pgvector similarity search.
32
+
33
+ ### Variable categories
34
+
35
+ | Category | Examples |
36
+ |----------|---------|
37
+ | `income` | Median household income, per-capita income, Gini index |
38
+ | `poverty` | Population below poverty line, poverty ratios |
39
+ | `housing` | Home values, gross rent, occupancy, homeownership |
40
+ | `employment` | Labor force participation, unemployment, industry |
41
+ | `demographics` | Total population, age, sex, race, Hispanic origin |
42
+ | `education` | High school, bachelor's, graduate degree attainment |
43
+ | `transportation` | Commute mode, work from home, travel time |
44
+ | `health_insurance` | Coverage by age and sex |
45
+
46
+ ---
47
+
48
+ ## Schema Indexing
49
+
50
+ After data load, `indexSchema()` from `@pgsage/core` generates natural-language descriptions of each table and column, embeds them with Voyage AI, and upserts into `schema_embeddings`. This enables the retriever to find relevant schema context via cosine similarity.
51
+
52
+ Re-index at any time:
53
+
54
+ ```sh
55
+ pnpm --filter @pgsage/loaders load:census # re-runs indexing automatically
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Adding New Datasets
61
+
62
+ Each loader follows the same pattern:
63
+
64
+ 1. Fetch data from the source API
65
+ 2. Upsert into `geography`, `variables`, and `estimates` tables
66
+ 3. Call `indexSchema()` to update embeddings
67
+
68
+ Add a new entry point in `src/<dataset>/index.ts` and a corresponding script in `package.json`. The retriever and planner automatically pick up any new schema content on next query.
69
+
70
+ ---
71
+
72
+ ## Testing
73
+
74
+ ```sh
75
+ pnpm --filter @pgsage/loaders test
76
+ ```
77
+
78
+ 51 unit tests covering API client, data transformation, and schema indexing logic.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Census Bureau ACS 5-Year API client.
3
+ *
4
+ * All functions use Node's built-in fetch (Node 22+). The Census API returns
5
+ * a 2D array of strings: row 0 is the header, subsequent rows are data.
6
+ * All numeric values come back as strings and must be parsed; null/suppressed
7
+ * data arrives as the string "null" or "-666666666".
8
+ */
9
+ import type { CensusConfig, CensusApiResponse, GeoRecord, EstimateRecord, VariableDefinition } from "./types.js";
10
+ /** FIPS code for Rhode Island — used in smoke-test mode. */
11
+ export declare const SMOKE_STATE_FIPS = "44";
12
+ /**
13
+ * Split an array into chunks of at most `size` elements.
14
+ */
15
+ export declare function chunk<T>(arr: T[], size: number): T[][];
16
+ /**
17
+ * Parse a Census API numeric string value.
18
+ * Returns null for "null", empty string, or the suppressed-value sentinel.
19
+ */
20
+ export declare function parseCensusNumber(raw: string | null | undefined): number | null;
21
+ /**
22
+ * Low-level Census API GET with retry on 429 and 5xx.
23
+ * Returns the parsed 2D string array from the Census JSON response.
24
+ */
25
+ export declare function censusGet(url: string, config: CensusConfig): Promise<CensusApiResponse>;
26
+ /**
27
+ * Fetch all states (and DC + PR) from the ACS.
28
+ * Returns one GeoRecord per state, level='state'.
29
+ */
30
+ export declare function fetchStates(config: CensusConfig): Promise<GeoRecord[]>;
31
+ /**
32
+ * Fetch all counties for a given set of state FIPS codes (or all states with '*').
33
+ * In smoke mode, only fetches counties for Rhode Island (FIPS 44).
34
+ */
35
+ export declare function fetchCounties(config: CensusConfig, stateFips?: string): Promise<GeoRecord[]>;
36
+ /**
37
+ * Fetch estimates for a set of variables at either state or county level.
38
+ *
39
+ * Batches variables into chunks of MAX_ESTIMATE_VARS_PER_REQUEST to stay
40
+ * within Census API limits (each _E code is paired with its _M code, so
41
+ * 24 estimate vars = 48 actual vars in the request, plus NAME = 49 total).
42
+ *
43
+ * Inserts a batchDelayMs pause between requests to be polite to the API.
44
+ */
45
+ export declare function fetchEstimates(config: CensusConfig, geoLevel: "state" | "county", variables: VariableDefinition[], stateFips?: string): Promise<EstimateRecord[]>;
46
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/census/api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAYpB,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAMrC;;GAEG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,EAAE,CAMtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAI/E;AAUD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwC7F;AAaD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAiB5E;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,EACpB,SAAS,GAAE,MAAY,GACtB,OAAO,CAAC,SAAS,EAAE,CAAC,CAsBtB;AAMD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,OAAO,GAAG,QAAQ,EAC5B,SAAS,EAAE,kBAAkB,EAAE,EAC/B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CA2D3B"}
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Census Bureau ACS 5-Year API client.
3
+ *
4
+ * All functions use Node's built-in fetch (Node 22+). The Census API returns
5
+ * a 2D array of strings: row 0 is the header, subsequent rows are data.
6
+ * All numeric values come back as strings and must be parsed; null/suppressed
7
+ * data arrives as the string "null" or "-666666666".
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Constants
11
+ // ---------------------------------------------------------------------------
12
+ /** Census suppression sentinel. Treat as null. */
13
+ const SUPPRESSED_VALUE = "-666666666";
14
+ /** Maximum number of _E variables per request (paired with _M = 50 total vars). */
15
+ const MAX_ESTIMATE_VARS_PER_REQUEST = 24;
16
+ /** FIPS code for Rhode Island — used in smoke-test mode. */
17
+ export const SMOKE_STATE_FIPS = "44";
18
+ // ---------------------------------------------------------------------------
19
+ // Internal helpers
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Split an array into chunks of at most `size` elements.
23
+ */
24
+ export function chunk(arr, size) {
25
+ const result = [];
26
+ for (let i = 0; i < arr.length; i += size) {
27
+ result.push(arr.slice(i, i + size));
28
+ }
29
+ return result;
30
+ }
31
+ /**
32
+ * Parse a Census API numeric string value.
33
+ * Returns null for "null", empty string, or the suppressed-value sentinel.
34
+ */
35
+ export function parseCensusNumber(raw) {
36
+ if (raw == null || raw === "" || raw === "null" || raw === SUPPRESSED_VALUE)
37
+ return null;
38
+ const n = Number(raw);
39
+ return Number.isNaN(n) ? null : n;
40
+ }
41
+ /**
42
+ * Sleep for `ms` milliseconds. Used to be polite to the Census API between
43
+ * batch requests.
44
+ */
45
+ function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+ /**
49
+ * Low-level Census API GET with retry on 429 and 5xx.
50
+ * Returns the parsed 2D string array from the Census JSON response.
51
+ */
52
+ export async function censusGet(url, config) {
53
+ const maxRetries = config.maxRetries ?? 3;
54
+ const timeoutMs = config.requestTimeoutMs ?? 30_000;
55
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
56
+ const signal = AbortSignal.timeout(timeoutMs);
57
+ let res;
58
+ try {
59
+ res = await fetch(url, { signal });
60
+ }
61
+ catch (err) {
62
+ if (attempt < maxRetries) {
63
+ await sleep(1000 * 2 ** attempt);
64
+ continue;
65
+ }
66
+ throw new Error(`Census API fetch failed after ${maxRetries + 1} attempts: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
67
+ }
68
+ if (res.status === 429 || res.status >= 500) {
69
+ if (attempt < maxRetries) {
70
+ await sleep(1000 * 2 ** attempt);
71
+ continue;
72
+ }
73
+ throw new Error(`Census API error ${res.status} after ${maxRetries + 1} attempts: ${url}`);
74
+ }
75
+ if (!res.ok) {
76
+ const body = await res.text().catch(() => "(no body)");
77
+ throw new Error(`Census API error ${res.status}: ${body}`);
78
+ }
79
+ const data = (await res.json());
80
+ return data;
81
+ }
82
+ // Unreachable, but TypeScript needs it
83
+ throw new Error("Census API: exhausted retries");
84
+ }
85
+ /**
86
+ * Build the base Census API URL for 5-Year ACS estimates.
87
+ */
88
+ function baseUrl(config) {
89
+ return `${config.baseUrl}/${config.vintage}/acs/acs5`;
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Geography fetchers
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * Fetch all states (and DC + PR) from the ACS.
96
+ * Returns one GeoRecord per state, level='state'.
97
+ */
98
+ export async function fetchStates(config) {
99
+ const url = `${baseUrl(config)}?get=NAME&for=state:*&key=${config.apiKey}`;
100
+ const rows = await censusGet(url, config);
101
+ // Row 0 is headers: ["NAME", "state"]
102
+ return rows.slice(1).map((row) => {
103
+ const [name, stateFips] = row;
104
+ return {
105
+ geoId: stateFips,
106
+ stateFips,
107
+ countyFips: null,
108
+ stateName: name,
109
+ countyName: null,
110
+ level: "state",
111
+ };
112
+ });
113
+ }
114
+ /**
115
+ * Fetch all counties for a given set of state FIPS codes (or all states with '*').
116
+ * In smoke mode, only fetches counties for Rhode Island (FIPS 44).
117
+ */
118
+ export async function fetchCounties(config, stateFips = "*") {
119
+ const inClause = stateFips === "*" ? "state:*" : `state:${stateFips}`;
120
+ const url = `${baseUrl(config)}?get=NAME&for=county:*&in=${inClause}&key=${config.apiKey}`;
121
+ const rows = await censusGet(url, config);
122
+ // Row 0 is headers: ["NAME", "state", "county"]
123
+ return rows.slice(1).map((row) => {
124
+ const [name, sFips, cFips] = row;
125
+ // NAME is "County Name, State Name" — split on ", " to get county name
126
+ const commaIdx = name.lastIndexOf(", ");
127
+ const countyName = commaIdx >= 0 ? name.slice(0, commaIdx) : name;
128
+ const stateName = commaIdx >= 0 ? name.slice(commaIdx + 2) : "";
129
+ return {
130
+ geoId: sFips + cFips, // 5-digit FIPS
131
+ stateFips: sFips,
132
+ countyFips: cFips,
133
+ stateName,
134
+ countyName,
135
+ level: "county",
136
+ };
137
+ });
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Estimates fetcher
141
+ // ---------------------------------------------------------------------------
142
+ /**
143
+ * Fetch estimates for a set of variables at either state or county level.
144
+ *
145
+ * Batches variables into chunks of MAX_ESTIMATE_VARS_PER_REQUEST to stay
146
+ * within Census API limits (each _E code is paired with its _M code, so
147
+ * 24 estimate vars = 48 actual vars in the request, plus NAME = 49 total).
148
+ *
149
+ * Inserts a batchDelayMs pause between requests to be polite to the API.
150
+ */
151
+ export async function fetchEstimates(config, geoLevel, variables, stateFips) {
152
+ const batchDelay = config.batchDelayMs ?? 200;
153
+ const batches = chunk(variables, MAX_ESTIMATE_VARS_PER_REQUEST);
154
+ const results = [];
155
+ for (let i = 0; i < batches.length; i++) {
156
+ if (i > 0)
157
+ await sleep(batchDelay);
158
+ const batch = batches[i];
159
+ const eCodes = batch.map((v) => v.code);
160
+ const mCodes = batch.map((v) => v.code.replace(/E$/, "M"));
161
+ const allCodes = [...eCodes, ...mCodes];
162
+ let forClause;
163
+ let inClause = null;
164
+ if (geoLevel === "state") {
165
+ // Restrict to a single state in smoke mode; otherwise fetch all states
166
+ forClause = stateFips ? `state:${stateFips}` : "state:*";
167
+ }
168
+ else {
169
+ // County — must add &in=state:* or restrict to one state
170
+ forClause = "county:*";
171
+ inClause = stateFips ? `state:${stateFips}` : "state:*";
172
+ }
173
+ const url = [
174
+ `${baseUrl(config)}?get=NAME,${allCodes.join(",")}`,
175
+ `&for=${forClause}`,
176
+ inClause ? `&in=${inClause}` : "",
177
+ `&key=${config.apiKey}`,
178
+ ].join("");
179
+ const rows = await censusGet(url, config);
180
+ // Row 0: ["NAME", ...eCodes, ...mCodes, "state"] or ["NAME", ..., "state", "county"]
181
+ const header = rows[0];
182
+ const stateIdx = header.lastIndexOf("state");
183
+ const countyIdx = header.indexOf("county");
184
+ for (const row of rows.slice(1)) {
185
+ const sFips = row[stateIdx] ?? "";
186
+ const cFips = countyIdx >= 0 ? (row[countyIdx] ?? null) : null;
187
+ const geoId = cFips ? sFips + cFips : sFips;
188
+ for (let j = 0; j < eCodes.length; j++) {
189
+ const eIdx = header.indexOf(eCodes[j]);
190
+ const mIdx = header.indexOf(mCodes[j]);
191
+ const estimate = parseCensusNumber(eIdx >= 0 ? row[eIdx] : null);
192
+ const marginError = parseCensusNumber(mIdx >= 0 ? row[mIdx] : null);
193
+ results.push({
194
+ geoId,
195
+ variableCode: eCodes[j],
196
+ estimate,
197
+ marginError,
198
+ });
199
+ }
200
+ }
201
+ }
202
+ return results;
203
+ }
204
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/census/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Census ACS Loader — CLI entrypoint
3
+ *
4
+ * Fetches 2024 ACS 5-Year data from the Census Bureau API and populates
5
+ * the local pgsage Postgres database, then indexes the schema into
6
+ * schema_embeddings via Voyage AI.
7
+ *
8
+ * Usage:
9
+ * pnpm --filter @pgsage/loaders load:census # full run (~3-5 min)
10
+ * pnpm --filter @pgsage/loaders load:census:smoke # smoke test (~10 sec)
11
+ * pnpm --filter @pgsage/loaders load:census -- --smoke
12
+ *
13
+ * Required env vars: CENSUS_API_KEY, DATABASE_URL, VOYAGE_API_KEY
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/census/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Census ACS Loader — CLI entrypoint
3
+ *
4
+ * Fetches 2024 ACS 5-Year data from the Census Bureau API and populates
5
+ * the local pgsage Postgres database, then indexes the schema into
6
+ * schema_embeddings via Voyage AI.
7
+ *
8
+ * Usage:
9
+ * pnpm --filter @pgsage/loaders load:census # full run (~3-5 min)
10
+ * pnpm --filter @pgsage/loaders load:census:smoke # smoke test (~10 sec)
11
+ * pnpm --filter @pgsage/loaders load:census -- --smoke
12
+ *
13
+ * Required env vars: CENSUS_API_KEY, DATABASE_URL, VOYAGE_API_KEY
14
+ */
15
+ import pg from "pg";
16
+ import { introspector, retriever, embeddings } from "@pgsage/core";
17
+ const { introspect } = introspector;
18
+ const { indexSchema } = retriever;
19
+ const { VoyageEmbeddingProvider } = embeddings;
20
+ import { fetchStates, fetchCounties, fetchEstimates, SMOKE_STATE_FIPS } from "./api.js";
21
+ import { upsertGeography, upsertVariables, upsertEstimates } from "./loader.js";
22
+ import { CENSUS_VARIABLES, SMOKE_TEST_VARIABLES } from "./variables.js";
23
+ // ---------------------------------------------------------------------------
24
+ // Entry point
25
+ // ---------------------------------------------------------------------------
26
+ const isSmoke = process.argv.includes("--smoke");
27
+ async function run() {
28
+ const startMs = Date.now();
29
+ // --- Validate environment -----------------------------------------------
30
+ const apiKey = process.env["CENSUS_API_KEY"];
31
+ const databaseUrl = process.env["DATABASE_URL"];
32
+ const voyageApiKey = process.env["VOYAGE_API_KEY"];
33
+ if (!apiKey)
34
+ throw new Error("CENSUS_API_KEY environment variable is required.");
35
+ if (!databaseUrl)
36
+ throw new Error("DATABASE_URL environment variable is required.");
37
+ if (!voyageApiKey)
38
+ throw new Error("VOYAGE_API_KEY environment variable is required.");
39
+ const config = {
40
+ apiKey,
41
+ vintage: 2024,
42
+ baseUrl: "https://api.census.gov/data",
43
+ databaseUrl,
44
+ voyageApiKey,
45
+ smoke: isSmoke,
46
+ batchDelayMs: 200,
47
+ maxRetries: 3,
48
+ requestTimeoutMs: 30_000,
49
+ };
50
+ const variables = isSmoke ? SMOKE_TEST_VARIABLES : CENSUS_VARIABLES;
51
+ const mode = isSmoke ? "SMOKE TEST" : "FULL";
52
+ console.log(`\npgsage Census ACS Loader — ${mode} mode`);
53
+ console.log(`Vintage: ${config.vintage} ACS 5-Year`);
54
+ console.log(`Variables: ${variables.length}`);
55
+ if (isSmoke) {
56
+ console.log(`Geography: Rhode Island only (state FIPS ${SMOKE_STATE_FIPS})`);
57
+ }
58
+ console.log("");
59
+ // --- Database setup -------------------------------------------------------
60
+ const pool = new pg.Pool({ connectionString: databaseUrl });
61
+ const summary = {
62
+ geographiesUpserted: 0,
63
+ variablesUpserted: 0,
64
+ estimatesUpserted: 0,
65
+ embeddingTokens: 0,
66
+ elapsedMs: 0,
67
+ };
68
+ try {
69
+ // --- 1. Geography -------------------------------------------------------
70
+ console.log("Step 1/6: Fetching states...");
71
+ const states = isSmoke
72
+ ? (await fetchStates(config)).filter((g) => g.stateFips === SMOKE_STATE_FIPS)
73
+ : await fetchStates(config);
74
+ summary.geographiesUpserted += await upsertGeography(pool, states);
75
+ console.log(` ✓ ${states.length} states upserted`);
76
+ console.log("Step 2/6: Fetching counties...");
77
+ const counties = await fetchCounties(config, isSmoke ? SMOKE_STATE_FIPS : "*");
78
+ summary.geographiesUpserted += await upsertGeography(pool, counties);
79
+ console.log(` ✓ ${counties.length} counties upserted`);
80
+ // --- 2. Variables -------------------------------------------------------
81
+ console.log("Step 3/6: Upserting variable definitions...");
82
+ summary.variablesUpserted = await upsertVariables(pool, variables);
83
+ console.log(` ✓ ${summary.variablesUpserted} variables upserted`);
84
+ // --- 3. Estimates — states ----------------------------------------------
85
+ console.log("Step 4/6: Fetching state-level estimates...");
86
+ const stateEstimates = await fetchEstimates(config, "state", variables, isSmoke ? SMOKE_STATE_FIPS : undefined);
87
+ summary.estimatesUpserted += await upsertEstimates(pool, stateEstimates);
88
+ console.log(` ✓ ${stateEstimates.length} state estimate rows upserted`);
89
+ // --- 4. Estimates — counties --------------------------------------------
90
+ console.log("Step 5/6: Fetching county-level estimates...");
91
+ const countyEstimates = await fetchEstimates(config, "county", variables, isSmoke ? SMOKE_STATE_FIPS : undefined);
92
+ summary.estimatesUpserted += await upsertEstimates(pool, countyEstimates);
93
+ console.log(` ✓ ${countyEstimates.length} county estimate rows upserted`);
94
+ // --- 5. Schema embedding -----------------------------------------------
95
+ console.log("Step 6/6: Introspecting schema and indexing embeddings...");
96
+ const schema = await introspect(pool, { schemas: ["public"] });
97
+ const embedder = new VoyageEmbeddingProvider({ apiKey: voyageApiKey });
98
+ const indexResult = await indexSchema(pool, schema, embedder);
99
+ summary.embeddingTokens = indexResult.totalTokens;
100
+ console.log(` ✓ ${indexResult.rowsUpserted} schema embedding rows upserted`);
101
+ console.log(` ✓ ${indexResult.totalTokens} embedding tokens consumed`);
102
+ // --- Summary ------------------------------------------------------------
103
+ summary.elapsedMs = Date.now() - startMs;
104
+ const elapsed = (summary.elapsedMs / 1000).toFixed(1);
105
+ console.log(`\n${"─".repeat(50)}`);
106
+ console.log(`Census load complete in ${elapsed}s`);
107
+ console.log(` Geographies : ${summary.geographiesUpserted}`);
108
+ console.log(` Variables : ${summary.variablesUpserted}`);
109
+ console.log(` Estimates : ${summary.estimatesUpserted}`);
110
+ console.log(` Emb. tokens : ${summary.embeddingTokens}`);
111
+ console.log(`${"─".repeat(50)}\n`);
112
+ }
113
+ finally {
114
+ await pool.end();
115
+ }
116
+ }
117
+ void run().catch((error) => {
118
+ console.error("\nCensus loader failed:");
119
+ console.error(error instanceof Error ? error.message : error);
120
+ process.exitCode = 1;
121
+ });
122
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/census/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Census DB loader — upserts GeoRecords, VariableDefinitions, and
3
+ * EstimateRecords into the pgsage Postgres schema.
4
+ *
5
+ * All three functions are idempotent: safe to re-run with the same data.
6
+ * Estimates are batched into chunks of ESTIMATE_BATCH_SIZE rows to avoid
7
+ * OOM on large payloads (~680K rows total for the full dataset).
8
+ */
9
+ import type pg from "pg";
10
+ import type { GeoRecord, VariableDefinition, EstimateRecord } from "./types.js";
11
+ /**
12
+ * Upsert geography records into the `geography` table.
13
+ * Uses INSERT ... ON CONFLICT (geo_id) DO UPDATE to handle re-runs.
14
+ * Batches inserts to avoid exceeding PostgreSQL's parameter limit.
15
+ *
16
+ * @returns Total number of rows upserted.
17
+ */
18
+ export declare function upsertGeography(pool: InstanceType<typeof pg.Pool>, records: GeoRecord[]): Promise<number>;
19
+ /**
20
+ * Upsert variable definitions into the `variables` table.
21
+ * The full curated list is only ~247 rows so a single upsert suffices.
22
+ *
23
+ * @returns Total number of rows upserted.
24
+ */
25
+ export declare function upsertVariables(pool: InstanceType<typeof pg.Pool>, definitions: VariableDefinition[]): Promise<number>;
26
+ /**
27
+ * Upsert estimate records into the `estimates` table.
28
+ * Batches rows to avoid OOM; each batch runs in a single transaction.
29
+ *
30
+ * @returns Total number of rows upserted.
31
+ */
32
+ export declare function upsertEstimates(pool: InstanceType<typeof pg.Pool>, records: EstimateRecord[]): Promise<number>;
33
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/census/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAahF;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAClC,OAAO,EAAE,SAAS,EAAE,GACnB,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAMD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAClC,WAAW,EAAE,kBAAkB,EAAE,GAChC,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAMD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAClC,OAAO,EAAE,cAAc,EAAE,GACxB,OAAO,CAAC,MAAM,CAAC,CAsCjB"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Census DB loader — upserts GeoRecords, VariableDefinitions, and
3
+ * EstimateRecords into the pgsage Postgres schema.
4
+ *
5
+ * All three functions are idempotent: safe to re-run with the same data.
6
+ * Estimates are batched into chunks of ESTIMATE_BATCH_SIZE rows to avoid
7
+ * OOM on large payloads (~680K rows total for the full dataset).
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Constants
11
+ // ---------------------------------------------------------------------------
12
+ const ESTIMATE_BATCH_SIZE = 500;
13
+ const GEO_BATCH_SIZE = 500;
14
+ // ---------------------------------------------------------------------------
15
+ // Geography
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Upsert geography records into the `geography` table.
19
+ * Uses INSERT ... ON CONFLICT (geo_id) DO UPDATE to handle re-runs.
20
+ * Batches inserts to avoid exceeding PostgreSQL's parameter limit.
21
+ *
22
+ * @returns Total number of rows upserted.
23
+ */
24
+ export async function upsertGeography(pool, records) {
25
+ if (records.length === 0)
26
+ return 0;
27
+ let total = 0;
28
+ const batches = chunkArray(records, GEO_BATCH_SIZE);
29
+ for (const batch of batches) {
30
+ // Build multi-row VALUES clause
31
+ const values = [];
32
+ const placeholders = batch.map((rec, i) => {
33
+ const base = i * 6;
34
+ values.push(rec.geoId, rec.stateFips, rec.countyFips ?? null, rec.stateName, rec.countyName ?? null, rec.level);
35
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6})`;
36
+ });
37
+ await pool.query(`INSERT INTO geography (geo_id, state_fips, county_fips, state_name, county_name, level)
38
+ VALUES ${placeholders.join(", ")}
39
+ ON CONFLICT (geo_id) DO UPDATE SET
40
+ state_fips = EXCLUDED.state_fips,
41
+ county_fips = EXCLUDED.county_fips,
42
+ state_name = EXCLUDED.state_name,
43
+ county_name = EXCLUDED.county_name,
44
+ level = EXCLUDED.level`, values);
45
+ total += batch.length;
46
+ }
47
+ return total;
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Variables
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * Upsert variable definitions into the `variables` table.
54
+ * The full curated list is only ~247 rows so a single upsert suffices.
55
+ *
56
+ * @returns Total number of rows upserted.
57
+ */
58
+ export async function upsertVariables(pool, definitions) {
59
+ if (definitions.length === 0)
60
+ return 0;
61
+ const values = [];
62
+ const placeholders = definitions.map((def, i) => {
63
+ const base = i * 4;
64
+ values.push(def.code, def.label, def.concept, def.category);
65
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4})`;
66
+ });
67
+ await pool.query(`INSERT INTO variables (variable_code, label, concept, category)
68
+ VALUES ${placeholders.join(", ")}
69
+ ON CONFLICT (variable_code) DO UPDATE SET
70
+ label = EXCLUDED.label,
71
+ concept = EXCLUDED.concept,
72
+ category = EXCLUDED.category`, values);
73
+ return definitions.length;
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Estimates
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * Upsert estimate records into the `estimates` table.
80
+ * Batches rows to avoid OOM; each batch runs in a single transaction.
81
+ *
82
+ * @returns Total number of rows upserted.
83
+ */
84
+ export async function upsertEstimates(pool, records) {
85
+ if (records.length === 0)
86
+ return 0;
87
+ let total = 0;
88
+ const batches = chunkArray(records, ESTIMATE_BATCH_SIZE);
89
+ for (const batch of batches) {
90
+ const client = await pool.connect();
91
+ try {
92
+ await client.query("BEGIN");
93
+ const values = [];
94
+ const placeholders = batch.map((rec, i) => {
95
+ const base = i * 4;
96
+ values.push(rec.geoId, rec.variableCode, rec.estimate ?? null, rec.marginError ?? null);
97
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4})`;
98
+ });
99
+ await client.query(`INSERT INTO estimates (geo_id, variable_code, estimate, margin_error)
100
+ VALUES ${placeholders.join(", ")}
101
+ ON CONFLICT (geo_id, variable_code) DO UPDATE SET
102
+ estimate = EXCLUDED.estimate,
103
+ margin_error = EXCLUDED.margin_error`, values);
104
+ await client.query("COMMIT");
105
+ total += batch.length;
106
+ }
107
+ catch (err) {
108
+ await client.query("ROLLBACK");
109
+ throw err;
110
+ }
111
+ finally {
112
+ client.release();
113
+ }
114
+ }
115
+ return total;
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Internal helpers
119
+ // ---------------------------------------------------------------------------
120
+ function chunkArray(arr, size) {
121
+ const result = [];
122
+ for (let i = 0; i < arr.length; i += size) {
123
+ result.push(arr.slice(i, i + size));
124
+ }
125
+ return result;
126
+ }
127
+ //# sourceMappingURL=loader.js.map