import { sql } from 'drizzle-orm'; import { boolean, index, integer, primaryKey, real, smallint, text, timestamp, uniqueIndex, type AnyPgColumn } from 'drizzle-orm/pg-core'; import { trainsSchema } from './schemas'; import { id, timestamps } from './helpers'; // Public train "pokedex" (/trackers/trains). Read-only catalogue built from open data by // scripts/trains-import.ts (re-runnable; upserts types by Wikidata id so Dex numbers stay // stable) and scripts/trains-photos.ts (Commons photos + maker/operator logos). Sources: // - English Wikipedia (CC BY-SA): every article with {{Infobox locomotive}}, {{Infobox train}} // or {{Infobox tram}} (builder, operators, years, number built, speed, power, gauge, ...). // - Wikidata (CC0): manufacturer (P176), operator (P137), country, number built, labels. // Only classes/types (individual locomotives and list articles are skipped). Unknown stays null. export const trainMakers = trainsSchema.table( 'makers', { id: id(), slug: text('slug').notNull(), name: text('name').notNull(), fullName: text('full_name'), country: text('country'), wikidata: text('wikidata'), wikipedia: text('wikipedia'), typeCount: integer('type_count').notNull().default(0), firstYear: smallint('first_year'), lastYear: smallint('last_year'), /** Sum of number built over the maker's types (families are not double counted). */ builtTotal: integer('built_total').notNull().default(0), sources: text('sources').array().notNull().default([]), /** Logo thumbnail (Wikimedia Commons, 250px) and its credit. */ logo: text('logo'), logoFile: text('logo_file'), logoAuthor: text('logo_author'), logoLicense: text('logo_license'), /** Hero photo: lead photo of the maker's most built type. */ photo: text('photo'), ...timestamps }, (t) => [uniqueIndex('makers_slug_key').on(t.slug)] ); export const trainOperators = trainsSchema.table( 'operators', { id: id(), slug: text('slug').notNull(), name: text('name').notNull(), fullName: text('full_name'), country: text('country'), wikidata: text('wikidata'), wikipedia: text('wikipedia'), typeCount: integer('type_count').notNull().default(0), logo: text('logo'), logoFile: text('logo_file'), logoAuthor: text('logo_author'), logoLicense: text('logo_license'), photo: text('photo'), ...timestamps }, (t) => [uniqueIndex('operators_slug_key').on(t.slug)] ); export const trainTypes = trainsSchema.table( 'types', { id: id(), dex: integer('dex').notNull(), makerId: integer('maker_id') .notNull() .references(() => trainMakers.id, { onDelete: 'cascade' }), /** Parent class in the dex (e.g. ICE 3 -> Velaro), from Wikidata "subclass of". */ familyId: integer('family_id').references((): AnyPgColumn => trainTypes.id, { onDelete: 'set null' }), slug: text('slug').notNull(), /** Name without the maker ("Velaro"); full_name is the article title ("Siemens Velaro"). */ name: text('name').notNull(), fullName: text('full_name').notNull(), /** Dutch label from Wikidata, when it differs. */ nlName: text('nl_name'), /** Lowercase names, aliases, maker, operators and class numbers for ILIKE / trigram search. */ search: text('search').notNull(), wikidata: text('wikidata').notNull(), wikipedia: text('wikipedia'), description: text('description'), /** First sentences of the Wikipedia article. */ summary: text('summary'), /** steam, diesel, electric (locomotives), high-speed, multiple unit, metro, tram, carriage, other. */ category: text('category').notNull(), /** Infobox stock type / power type ("Electric multiple unit"). */ categoryLabel: text('category_label'), /** steam, diesel, electric, battery, hydrogen, gas turbine, bi-mode, none. */ traction: text('traction'), country: text('country'), /** Country of origin plus the countries of its operators. */ countries: text('countries').array().notNull().default([]), makers: text('makers').array().notNull().default([]), operators: text('operators').array().notNull().default([]), firstBuilt: smallint('first_built'), lastBuilt: smallint('last_built'), introducedYear: smallint('introduced_year'), retiredYear: smallint('retired_year'), /** in service, retired, preserved. */ status: text('status'), inService: boolean('in_service'), numberBuilt: integer('number_built'), maxSpeedKmh: smallint('max_speed_kmh'), powerKw: integer('power_kw'), gauge: text('gauge'), gaugeMm: smallint('gauge_mm'), electricSystem: text('electric_system'), /** Whyte (4-6-2) for steam, else UIC (Bo'Bo'). */ wheels: text('wheels'), lengthM: real('length_m'), weightT: real('weight_t'), capacity: smallint('capacity'), formation: text('formation'), designer: text('designer'), nicknames: text('nicknames'), sources: text('sources').array().notNull().default([]), /** Lead photo thumbnail URL (960px), denormalised from trains.photos for the card grid. */ photo: text('photo'), photoCount: smallint('photo_count').notNull().default(0), photosCheckedAt: timestamp('photos_checked_at', { withTimezone: true }), ...timestamps }, (t) => [ uniqueIndex('types_dex_key').on(t.dex), uniqueIndex('types_wikidata_key').on(t.wikidata), uniqueIndex('types_maker_slug_key').on(t.makerId, t.slug), index('types_number_built_idx').on(t.numberBuilt), index('types_first_built_idx').on(t.firstBuilt), index('types_category_idx').on(t.category), index('types_search_trgm_idx').using('gin', sql`${t.search} public.gin_trgm_ops`) ] ); export const trainTypeOperators = trainsSchema.table( 'type_operators', { typeId: integer('type_id') .notNull() .references(() => trainTypes.id, { onDelete: 'cascade' }), operatorId: integer('operator_id') .notNull() .references(() => trainOperators.id, { onDelete: 'cascade' }), position: smallint('position').notNull().default(0) }, (t) => [ primaryKey({ columns: [t.typeId, t.operatorId] }), index('type_operators_operator_idx').on(t.operatorId) ] ); /** Freely licensed photos from Wikimedia Commons; only thumbnail URLs are stored, nothing is downloaded. */ export const trainPhotos = trainsSchema.table( 'photos', { id: id(), typeId: integer('type_id') .notNull() .references(() => trainTypes.id, { onDelete: 'cascade' }), position: smallint('position').notNull(), /** lead (Wikidata image), infobox (article infobox), commons (Commons category), article (enwiki). */ kind: text('kind').notNull(), file: text('file').notNull(), caption: text('caption'), thumb: text('thumb').notNull(), width: integer('width').notNull(), height: integer('height').notNull(), author: text('author'), license: text('license'), licenseUrl: text('license_url') }, (t) => [uniqueIndex('photos_type_position_key').on(t.typeId, t.position)] );