Skip to content

Commit 64974ab

Browse files
Refactor tag count
1 parent ec32816 commit 64974ab

7 files changed

Lines changed: 759 additions & 64 deletions

File tree

server/catalog/npm-sync-service.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
createReplaceTagStatements,
1616
createPackageUrl,
1717
createRebuildPackageSearchStatements,
18+
createRebuildTagStatsStatements,
1819
createRefreshPackageSearchStatementsForPackages,
20+
createRefreshTagStatsStatements,
1921
createUpsertPackageStatement,
2022
type CatalogPackageRecord,
2123
} from "./package-store"
@@ -26,6 +28,7 @@ import {
2628
hasUnpublishedRegistryMarker,
2729
isSecurityHoldingPackage,
2830
} from "./package-removal"
31+
import { normalizeTagValue } from "./tag-normalization"
2932

3033
const DEFAULT_DOWNLOADS_PERIOD = "last-month"
3134
const DEFAULT_GITHUB_BATCH_SIZE = 50
@@ -210,6 +213,10 @@ export async function syncNpmCatalog(
210213
const batch = packageMetadata.slice(index, index + DEFAULT_WRITE_BATCH_SIZE)
211214
writeTasks.push(
212215
writeQueue.add(async () => {
216+
const batchPackageNames = batch.map((item) => item.packageName)
217+
const affectedTagIds = new Set(
218+
await loadExistingTagIdsForPackages(client, batchPackageNames)
219+
)
213220
const statements = batch.flatMap((item) => {
214221
const repositoryRef = parseGitHubRepositoryRef(item.repositoryUrl)
215222
const enrichmentOutcome = item.isRemoved
@@ -255,6 +262,9 @@ export async function syncNpmCatalog(
255262
item.packageTags
256263
),
257264
]
265+
collectNormalizedTagIds(item.packageTags).forEach((tagId) => {
266+
affectedTagIds.add(tagId)
267+
})
258268

259269
if (enrichmentOutcome.kind === "replace") {
260270
statements.push(
@@ -264,17 +274,24 @@ export async function syncNpmCatalog(
264274
enrichmentOutcome.repositoryTags
265275
)
266276
)
277+
collectNormalizedTagIds(enrichmentOutcome.repositoryTags).forEach(
278+
(tagId) => {
279+
affectedTagIds.add(tagId)
280+
}
281+
)
267282
}
268283

269284
return statements
270285
})
271286
statements.push(
272-
...createRefreshPackageSearchStatementsForPackages(
273-
batch.map((item) => item.packageName)
274-
)
287+
...createRefreshPackageSearchStatementsForPackages(batchPackageNames)
275288
)
276289

277290
await client.batch(statements, "write")
291+
await client.batch(
292+
createRefreshTagStatsStatements(Array.from(affectedTagIds)),
293+
"write"
294+
)
278295
storedCount += batch.length
279296

280297
const now = Date.now()
@@ -298,6 +315,8 @@ export async function syncNpmCatalog(
298315
await pruneOrphanedTags(client)
299316
options.onProgress?.("Rebuilding package search index.")
300317
await client.batch(createRebuildPackageSearchStatements(), "write")
318+
options.onProgress?.("Rebuilding tag stats.")
319+
await client.batch(createRebuildTagStatsStatements(), "write")
301320

302321
options.onProgress?.(`Stored ${packageMetadata.length} npm packages total.`)
303322

@@ -492,10 +511,55 @@ async function loadExistingRepositoryUrls(
492511
return repositoryUrls
493512
}
494513

514+
async function loadExistingTagIdsForPackages(
515+
client: CatalogDatabaseClient,
516+
packageNames: string[]
517+
) {
518+
const uniquePackageNames = Array.from(new Set(packageNames))
519+
520+
if (uniquePackageNames.length === 0) {
521+
return [] as string[]
522+
}
523+
524+
const placeholders = uniquePackageNames.map(() => "?").join(", ")
525+
const result = await client.execute({
526+
sql: `
527+
SELECT DISTINCT tag_id
528+
FROM (
529+
SELECT tag_id
530+
FROM package_tags
531+
WHERE package_name IN (${placeholders})
532+
UNION ALL
533+
SELECT tag_id
534+
FROM repository_tags
535+
WHERE package_name IN (${placeholders})
536+
)
537+
`,
538+
args: [...uniquePackageNames, ...uniquePackageNames],
539+
})
540+
541+
return result.rows.map((row) => String(row.tag_id))
542+
}
543+
495544
function normalizeStoredUrl(value: unknown) {
496545
return typeof value === "string" ? value : null
497546
}
498547

548+
function collectNormalizedTagIds(rawTags: string[]) {
549+
return Array.from(
550+
new Set(
551+
rawTags.flatMap((rawTag) => {
552+
const normalizedRawTag = normalizeOptionalString(rawTag)
553+
const normalizedTagId = normalizedRawTag
554+
? normalizeTagValue(normalizedRawTag)
555+
: undefined
556+
557+
return normalizedTagId ? [normalizedTagId] : []
558+
})
559+
)
560+
)
561+
}
562+
499563
function normalizeDownloadCountSelection(
500564
selection:
501565
| number
@@ -751,8 +815,9 @@ async function fetchNpmPackageMetadata(
751815
})
752816
) {
753817
const npmViewResolves =
754-
(await options.npmViewRunner?.(packageName)) ??
755-
(await resolvePackageWithNpmView(packageName))
818+
typeof options.npmViewRunner === "function"
819+
? await options.npmViewRunner(packageName)
820+
: await resolvePackageWithNpmView(packageName)
756821

757822
if (npmViewResolves === null) {
758823
options.onProgress?.(

server/catalog/package-store.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { InStatement } from "@libsql/client"
22

33
import type { CatalogDatabaseClient } from "./database"
4+
import { createVisiblePackageSql } from "./package-removal"
45
import { createTagLabel, normalizeTagValue } from "./tag-normalization"
56

67
export type CatalogPackageRecord = {
@@ -43,6 +44,7 @@ export async function upsertPackage(
4344
[
4445
createUpsertPackageStatement(packageRecord),
4546
...createRefreshPackageSearchStatements(packageRecord.packageName),
47+
...createRebuildTagStatsStatements(),
4648
],
4749
"write"
4850
)
@@ -58,6 +60,7 @@ export async function replacePackageTags(
5860
[
5961
...createReplaceTagStatements(tableName, packageName, rawTags),
6062
...createRefreshPackageSearchStatements(packageName),
63+
...createRebuildTagStatsStatements(),
6164
],
6265
"write"
6366
)
@@ -229,6 +232,93 @@ export function createRebuildPackageSearchStatements(): InStatement[] {
229232
]
230233
}
231234

235+
export function createRebuildTagStatsStatements(): InStatement[] {
236+
return [
237+
{
238+
sql: `DELETE FROM tag_stats`,
239+
},
240+
{
241+
sql: `
242+
INSERT INTO tag_stats (tag_id, package_count)
243+
WITH visible_tag_pairs AS (
244+
SELECT DISTINCT pt.package_name, pt.tag_id
245+
FROM package_tags pt
246+
JOIN packages p ON p.package_name = pt.package_name
247+
WHERE ${createVisiblePackageSql("p")}
248+
UNION
249+
SELECT DISTINCT rt.package_name, rt.tag_id
250+
FROM repository_tags rt
251+
JOIN packages p ON p.package_name = rt.package_name
252+
WHERE ${createVisiblePackageSql("p")}
253+
)
254+
SELECT tag_id, COUNT(*) AS package_count
255+
FROM visible_tag_pairs
256+
GROUP BY tag_id
257+
`,
258+
},
259+
...createBumpTagsVersionStatements(),
260+
]
261+
}
262+
263+
export function createRefreshTagStatsStatements(
264+
tagIds: string[]
265+
): InStatement[] {
266+
const uniqueTagIds = Array.from(new Set(tagIds))
267+
268+
if (uniqueTagIds.length === 0) {
269+
return []
270+
}
271+
272+
const placeholders = uniqueTagIds.map(() => "?").join(", ")
273+
const valuesClause = uniqueTagIds.map(() => "(?)").join(", ")
274+
275+
return [
276+
{
277+
sql: `DELETE FROM tag_stats WHERE tag_id IN (${placeholders})`,
278+
args: uniqueTagIds,
279+
},
280+
{
281+
sql: `
282+
WITH affected_tags(tag_id) AS (
283+
VALUES ${valuesClause}
284+
),
285+
visible_tag_pairs AS (
286+
SELECT DISTINCT pt.package_name, pt.tag_id
287+
FROM package_tags pt
288+
JOIN affected_tags at ON at.tag_id = pt.tag_id
289+
JOIN packages p ON p.package_name = pt.package_name
290+
WHERE ${createVisiblePackageSql("p")}
291+
UNION
292+
SELECT DISTINCT rt.package_name, rt.tag_id
293+
FROM repository_tags rt
294+
JOIN affected_tags at ON at.tag_id = rt.tag_id
295+
JOIN packages p ON p.package_name = rt.package_name
296+
WHERE ${createVisiblePackageSql("p")}
297+
)
298+
INSERT INTO tag_stats (tag_id, package_count)
299+
SELECT tag_id, COUNT(*) AS package_count
300+
FROM visible_tag_pairs
301+
GROUP BY tag_id
302+
`,
303+
args: uniqueTagIds,
304+
},
305+
...createBumpTagsVersionStatements(),
306+
]
307+
}
308+
309+
export function createBumpTagsVersionStatements(): InStatement[] {
310+
return [
311+
{
312+
sql: `
313+
INSERT INTO catalog_meta (meta_key, meta_value)
314+
VALUES ('tags_version', '1')
315+
ON CONFLICT(meta_key) DO UPDATE SET
316+
meta_value = CAST(COALESCE(CAST(meta_value AS INTEGER), 0) + 1 AS TEXT)
317+
`,
318+
},
319+
]
320+
}
321+
232322
const PACKAGE_SEARCH_TEXT_SQL = `
233323
LOWER(
234324
TRIM(

server/catalog/read-service.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -112,25 +112,13 @@ export async function listCatalogTags(
112112

113113
const result = await client.execute({
114114
sql: `
115-
WITH visible_tag_pairs AS (
116-
SELECT DISTINCT pt.package_name, pt.tag_id
117-
FROM package_tags pt
118-
JOIN packages p ON p.package_name = pt.package_name
119-
WHERE ${createVisiblePackageSql("p")}
120-
UNION
121-
SELECT DISTINCT rt.package_name, rt.tag_id
122-
FROM repository_tags rt
123-
JOIN packages p ON p.package_name = rt.package_name
124-
WHERE ${createVisiblePackageSql("p")}
125-
)
126115
SELECT
127-
vtp.tag_id,
116+
ts.tag_id,
128117
t.label,
129-
COUNT(*) AS package_count
130-
FROM visible_tag_pairs vtp
131-
JOIN tags t ON t.tag_id = vtp.tag_id
132-
GROUP BY vtp.tag_id, t.label
133-
ORDER BY package_count DESC, t.tag_id ASC
118+
ts.package_count
119+
FROM tag_stats ts
120+
JOIN tags t ON t.tag_id = ts.tag_id
121+
ORDER BY ts.package_count DESC, t.label COLLATE NOCASE ASC, ts.tag_id ASC
134122
LIMIT 500
135123
`,
136124
})

server/catalog/schema.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { InStatement } from "@libsql/client"
22

33
import type { CatalogDatabaseClient } from "./database"
4-
import { createRebuildPackageSearchStatements } from "./package-store"
4+
import {
5+
createRebuildPackageSearchStatements,
6+
createRebuildTagStatsStatements,
7+
} from "./package-store"
58

69
const EXPECTED_TABLE_COLUMNS = {
710
packages: [
@@ -20,6 +23,8 @@ const EXPECTED_TABLE_COLUMNS = {
2023
repository_tags: ["package_name", "tag_id", "raw_value"],
2124
tags: ["tag_id", "label"],
2225
tag_aliases: ["alias", "tag_id"],
26+
tag_stats: ["tag_id", "package_count"],
27+
catalog_meta: ["meta_key", "meta_value"],
2328
} as const
2429

2530
const schemaStatements: InStatement[] = [
@@ -70,6 +75,24 @@ const schemaStatements: InStatement[] = [
7075
FOREIGN KEY (tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE
7176
)
7277
`,
78+
`
79+
CREATE TABLE IF NOT EXISTS tag_stats (
80+
tag_id TEXT PRIMARY KEY,
81+
package_count INTEGER NOT NULL,
82+
FOREIGN KEY (tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE
83+
)
84+
`,
85+
`
86+
CREATE TABLE IF NOT EXISTS catalog_meta (
87+
meta_key TEXT PRIMARY KEY,
88+
meta_value TEXT NOT NULL
89+
)
90+
`,
91+
`
92+
INSERT INTO catalog_meta (meta_key, meta_value)
93+
VALUES ('tags_version', '0')
94+
ON CONFLICT(meta_key) DO NOTHING
95+
`,
7396
`
7497
CREATE VIRTUAL TABLE IF NOT EXISTS package_search_fts
7598
USING fts5(
@@ -139,6 +162,8 @@ const destructiveResetStatements: InStatement[] = [
139162
"DROP TABLE IF EXISTS package_tags",
140163
"DROP TABLE IF EXISTS package_search_fts",
141164
"DROP TABLE IF EXISTS tag_aliases",
165+
"DROP TABLE IF EXISTS tag_stats",
166+
"DROP TABLE IF EXISTS catalog_meta",
142167
"DROP TABLE IF EXISTS tags",
143168
"DROP TABLE IF EXISTS packages",
144169
"DROP TABLE IF EXISTS raw_ecosystems_packages",
@@ -153,12 +178,14 @@ export async function ensureCatalogSchema(client: CatalogDatabaseClient) {
153178
await applyStatements(client, obsoleteIndexDropStatements)
154179
await applyStatements(client, schemaStatements)
155180
await applyStatements(client, createRebuildPackageSearchStatements())
181+
await applyStatements(client, createRebuildTagStatsStatements())
156182
}
157183

158184
export async function resetCatalogSchema(client: CatalogDatabaseClient) {
159185
await applyStatements(client, destructiveResetStatements)
160186
await applyStatements(client, schemaStatements)
161187
await applyStatements(client, createRebuildPackageSearchStatements())
188+
await applyStatements(client, createRebuildTagStatsStatements())
162189
}
163190

164191
async function applyStatements(

0 commit comments

Comments
 (0)