Skip to content

Commit 53fa333

Browse files
authored
fix: cargo dump failure due to schema changes [CM-1282] (#4247)
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent d64cde5 commit 53fa333

1 file changed

Lines changed: 58 additions & 54 deletions

File tree

services/apps/packages_worker/src/cargo/loadDump.ts

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createReadStream } from 'node:fs'
22
import * as path from 'node:path'
3+
import { createInterface } from 'node:readline'
34
import { Transform } from 'node:stream'
45
import { pipeline } from 'node:stream/promises'
56
import { CopyStreamQuery, from as copyFrom } from 'pg-copy-streams'
@@ -14,55 +15,59 @@ const log = getServiceChildLogger('cargo-load')
1415

1516
export const STAGING_SCHEMA = 'cargo_sync'
1617

17-
// Column order matches each CSV header exactly so COPY FROM STDIN CSV HEADER maps by position.
18-
const STAGING_DDL = `
19-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.crates CASCADE;
20-
CREATE TABLE ${STAGING_SCHEMA}.crates (
21-
created_at text, description text, documentation text, homepage text,
22-
id integer, max_features text, max_upload_size text, name text,
23-
readme text, repository text, trustpub_only text, updated_at text
24-
);
25-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.versions CASCADE;
26-
CREATE TABLE ${STAGING_SCHEMA}.versions (
27-
bin_names text, categories text, checksum text, crate_id integer,
28-
crate_size text, created_at text, description text, documentation text,
29-
downloads text, edition text, features text, has_lib text, homepage text,
30-
id integer, keywords text, license text, links text, num text,
31-
num_no_build text, published_by text, repository text, rust_version text,
32-
updated_at text, yanked text
33-
);
34-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.default_versions CASCADE;
35-
CREATE TABLE ${STAGING_SCHEMA}.default_versions (
36-
crate_id integer, num_versions integer, version_id integer
37-
);
38-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.version_downloads CASCADE;
39-
CREATE TABLE ${STAGING_SCHEMA}.version_downloads (
40-
date date, downloads integer, version_id integer
41-
);
42-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.dependencies CASCADE;
43-
CREATE TABLE ${STAGING_SCHEMA}.dependencies (
44-
crate_id integer, default_features text, explicit_name text, features text,
45-
id integer, kind integer, optional text, req text, target text,
46-
version_id integer
47-
);
48-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.keywords CASCADE;
49-
CREATE TABLE ${STAGING_SCHEMA}.keywords (
50-
crates_cnt integer, created_at text, id integer, keyword text
51-
);
52-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.crates_keywords CASCADE;
53-
CREATE TABLE ${STAGING_SCHEMA}.crates_keywords (
54-
crate_id integer, keyword_id integer
55-
);
56-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.crate_owners CASCADE;
57-
CREATE TABLE ${STAGING_SCHEMA}.crate_owners (
58-
crate_id integer, created_at text, created_by text, owner_id integer,
59-
owner_kind integer
60-
);
61-
DROP TABLE IF EXISTS ${STAGING_SCHEMA}.oauth_github CASCADE;
62-
CREATE TABLE ${STAGING_SCHEMA}.oauth_github (
63-
account_id text, avatar text, login text, user_id integer
64-
);
65-
`
18+
const COLUMN_TYPES: Record<string, Record<string, string>> = {
19+
crates: { id: 'integer' },
20+
versions: { crate_id: 'integer', id: 'integer' },
21+
default_versions: { crate_id: 'integer', num_versions: 'integer', version_id: 'integer' },
22+
version_downloads: { date: 'date', downloads: 'integer', version_id: 'integer' },
23+
dependencies: { crate_id: 'integer', id: 'integer', kind: 'integer', version_id: 'integer' },
24+
keywords: { crates_cnt: 'integer', id: 'integer' },
25+
crates_keywords: { crate_id: 'integer', keyword_id: 'integer' },
26+
crate_owners: { crate_id: 'integer', owner_id: 'integer', owner_kind: 'integer' },
27+
oauth_github: { user_id: 'integer' },
28+
}
29+
30+
const REQUIRED_COLUMNS: Record<string, string[]> = {
31+
crates: ['id', 'name', 'description', 'homepage', 'repository'],
32+
versions: ['id', 'crate_id', 'created_at', 'num', 'license', 'yanked'],
33+
default_versions: ['crate_id', 'version_id'],
34+
version_downloads: ['date', 'downloads', 'version_id'],
35+
dependencies: ['crate_id', 'version_id', 'kind'],
36+
keywords: ['id', 'keyword'],
37+
crates_keywords: ['crate_id', 'keyword_id'],
38+
crate_owners: ['crate_id', 'owner_id', 'owner_kind'],
39+
oauth_github: ['user_id', 'login'],
40+
}
41+
42+
async function readCsvHeader(csvPath: string): Promise<string[]> {
43+
const source = createReadStream(csvPath)
44+
const rl = createInterface({ input: source, crlfDelay: Infinity })
45+
try {
46+
for await (const line of rl) {
47+
return line.split(',').map((c) => c.trim())
48+
}
49+
} finally {
50+
rl.close()
51+
source.destroy()
52+
}
53+
throw new Error(`CSV has no header line: ${csvPath}`)
54+
}
55+
56+
async function buildStagingTable(qx: QueryExecutor, table: string, csvPath: string): Promise<void> {
57+
const cols = await readCsvHeader(csvPath)
58+
const missing = (REQUIRED_COLUMNS[table] ?? []).filter((c) => !cols.includes(c))
59+
if (missing.length) {
60+
throw new Error(
61+
`cargo dump ${table}.csv is missing required column(s) [${missing.join(', ')}] — ` +
62+
`crates.io schema changed; header was [${cols.join(', ')}]`,
63+
)
64+
}
65+
const types = COLUMN_TYPES[table] ?? {}
66+
const ddl = cols.map((c) => `"${c.replace(/"/g, '""')}" ${types[c] ?? 'text'}`).join(', ')
67+
await qx.result(
68+
`DROP TABLE IF EXISTS ${STAGING_SCHEMA}.${table} CASCADE; CREATE TABLE ${STAGING_SCHEMA}.${table} (${ddl})`,
69+
)
70+
}
6671

6772
// crates.csv needs NUL stripping — readme blobs contain 0x00 bytes that COPY rejects.
6873
const CSV_FILES: Array<{ table: string; file: string; stripNul?: boolean }> = [
@@ -265,14 +270,13 @@ export async function loadDump(
265270
const dataDir = path.join(dumpDir, 'data')
266271

267272
log.info('Creating staging schema...')
268-
await qx.result(
269-
`CREATE SCHEMA IF NOT EXISTS ${STAGING_SCHEMA};
270-
${STAGING_DDL}`,
271-
)
273+
await qx.result(`CREATE SCHEMA IF NOT EXISTS ${STAGING_SCHEMA}`)
272274

273275
const counts: Record<string, number> = {}
274276
for (const { table, file, stripNul: doStripNul } of CSV_FILES) {
275-
counts[table] = await copyCsv(db, table, path.join(dataDir, file), doStripNul ?? false)
277+
const csvPath = path.join(dataDir, file)
278+
await buildStagingTable(qx, table, csvPath)
279+
counts[table] = await copyCsv(db, table, csvPath, doStripNul ?? false)
276280
log.info({ table, rows: counts[table] }, 'Loaded staging table')
277281
}
278282

0 commit comments

Comments
 (0)