feat(reference): add indexed readers and station source

This commit is contained in:
jb
2026-09-19 09:53:46 -04:00
parent 9ddd12fbc5
commit f7b604f40f
10 changed files with 429 additions and 25 deletions
+27 -4
View File
@@ -9,13 +9,14 @@ import type {
ReferenceType,
} from './types';
export const SCHEMA_VERSION = 1;
export const SCHEMA_VERSION = 3;
/**
* The database is built from scratch and opened read-only by the serving side.
* Foreign keys are deliberately explicit even for currently empty future tables.
*/
export function createReferenceSchema(db: Database): void {
db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
db.run('PRAGMA foreign_keys = ON');
db.run('PRAGMA journal_mode = DELETE');
db.run('PRAGMA synchronous = FULL');
@@ -56,6 +57,13 @@ export function createReferenceSchema(db: Database): void {
radius REAL,
base_price REAL
);
CREATE TABLE type_search (
type_id INTEGER NOT NULL REFERENCES types(type_id),
language TEXT NOT NULL,
name TEXT NOT NULL,
normalized_name TEXT NOT NULL,
PRIMARY KEY (type_id, language)
);
CREATE TABLE market_groups (
market_group_id INTEGER PRIMARY KEY,
parent_group_id INTEGER REFERENCES market_groups(market_group_id),
@@ -161,7 +169,8 @@ export function createReferenceSchema(db: Database): void {
owner_id INTEGER,
x REAL,
y REAL,
z REAL
z REAL,
source_verified INTEGER NOT NULL CHECK (source_verified IN (0, 1))
);
CREATE TABLE source_provenance (
source TEXT PRIMARY KEY,
@@ -176,6 +185,8 @@ export function createReferenceSchema(db: Database): void {
CREATE INDEX types_group_idx ON types(group_id);
CREATE INDEX types_category_idx ON types(category_id);
CREATE INDEX types_name_idx ON types(name_json);
CREATE INDEX type_search_normalized_name_idx ON type_search(normalized_name);
CREATE VIRTUAL TABLE type_search_fts USING fts5(type_id UNINDEXED, language UNINDEXED, name, normalized_name);
CREATE INDEX market_groups_parent_idx ON market_groups(parent_group_id);
CREATE INDEX activities_blueprint_idx ON activities(blueprint_type_id);
CREATE INDEX materials_type_idx ON materials(type_id);
@@ -196,6 +207,7 @@ export function createReferenceSchema(db: Database): void {
const bool = (value: boolean | undefined): number | null => value === undefined ? null : value ? 1 : 0;
const json = (value: unknown): string => JSON.stringify(value);
const normalizedName = (value: string): string => value.normalize('NFKC').trim().replace(/\s+/g, ' ').toLocaleLowerCase('en-US');
const keyId = (key: string, label: string): number => {
const id = Number(key);
if (!Number.isSafeInteger(id) || id < 0) throw new Error(`${label} key is not a safe integer: ${key}`);
@@ -229,6 +241,9 @@ export function insertReferenceData(db: Database, data: ReferenceDataPayload): v
type.run(id, row.group_id, row.category_id, json(row.name), row.description === undefined ? null : json(row.description), bool(row.published), bool(row.is_blueprint), bool(row.is_skill), bool(row.is_ore), bool(row.is_repackable), row.volume ?? null, row.packaged_volume ?? null, row.portion_size ?? null, row.mass ?? null, row.capacity ?? null, row.radius ?? null, row.base_price ?? null);
}
insertTypeMaps(db, data.types);
const typeSearch = db.prepare('INSERT INTO type_search(type_id,language,name,normalized_name) VALUES (?,?,?,?)');
const typeSearchFts = db.prepare('INSERT INTO type_search_fts(type_id,language,name,normalized_name) VALUES (?,?,?,?)');
for (const row of Object.values(data.types)) for (const [language, name] of Object.entries(row.name)) { typeSearch.run(row.type_id, language, name, normalizedName(name)); typeSearchFts.run(row.type_id, language, name, normalizedName(name)); }
const marketGroup = db.prepare('INSERT INTO market_groups(market_group_id,parent_group_id,name_json,description_json,icon_id,has_types) VALUES (?,?,?,?,?,?)');
for (const [key, value] of Object.entries(data.market_groups ?? {})) {
const row = value as Record<string, unknown>;
@@ -279,8 +294,16 @@ export function insertUniverseData(db: Database, data: EsiUniversePayload): void
}
const gate = db.prepare('INSERT INTO gates(system_id,stargate_id,destination_system_id) VALUES (?,?,?)');
for (const row of Object.values(data.gates ?? {})) gate.run(row.system_id, row.stargate_id, row.destination_system_id);
const station = db.prepare('INSERT INTO stations(station_id,system_id,name,type_id,owner_id,x,y,z) VALUES (?,?,?,?,?,?,?,?)');
for (const row of Object.values(data.systems)) for (const stationId of row.stations ?? []) station.run(stationId, row.system_id, null, null, null, null, null, null);
const station = db.prepare('INSERT INTO stations(station_id,system_id,name,type_id,owner_id,x,y,z,source_verified) VALUES (?,?,?,?,?,?,?,?,?)');
const stationIds = new Set<number>();
for (const row of Object.values(data.stations ?? {})) {
const position = row.position;
station.run(row.station_id, row.system_id, row.name ?? null, row.type_id ?? null, row.owner_id ?? null, position?.x ?? null, position?.y ?? null, position?.z ?? null, 1);
stationIds.add(row.station_id);
}
// ESI system responses expose station IDs but not station metadata. Preserve
// those IDs as explicitly unknown rather than fabricating a name or type.
for (const row of Object.values(data.systems)) for (const stationId of row.stations ?? []) if (!stationIds.has(stationId)) station.run(stationId, row.system_id, null, null, null, null, null, null, 0);
}
export function insertHoboleaksData(db: Database, data: HoboleaksPayload): void {