All checks were successful
Deploy production / deploy (push) Successful in 3m0s
181 lines
7.1 KiB
TypeScript
181 lines
7.1 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
|
import {
|
|
boolean,
|
|
index,
|
|
integer,
|
|
jsonb,
|
|
real,
|
|
smallint,
|
|
text,
|
|
timestamp,
|
|
uniqueIndex
|
|
} from 'drizzle-orm/pg-core';
|
|
import { carsSchema } from './schemas';
|
|
import { id, timestamps } from './helpers';
|
|
|
|
// Public car "pokedex" (/trackers/cars). Read-only catalogue built from open data by
|
|
// scripts/cars-import.ts (re-runnable; upserts brands/models by slug so Dex numbers stay
|
|
// stable, and replaces variants). Sources:
|
|
// - fueleconomy.gov vehicles.csv (US EPA, 1984-now): one `variants` row per EPA test car.
|
|
// - RDW open data (opendata.rdw.nl, Gekentekende voertuigen + brandstof): per make/model
|
|
// aggregates of passenger cars registered in NL (count, years, list price, mass, body),
|
|
// plus fuel/power/consumption read from one sample registration per model-year.
|
|
// Unknown stays null; nothing is estimated.
|
|
|
|
/** Shape of `models.nl_years`: registrations per first-registration year. */
|
|
export type CarNlYear = { y: number; n: number; price: number | null };
|
|
/** Shape of `models.nl_names`: RDW trade names (handelsbenaming) folded into this model. */
|
|
export type CarNlName = { name: string; n: number };
|
|
|
|
export const carBrands = carsSchema.table(
|
|
'brands',
|
|
{
|
|
id: id(),
|
|
slug: text('slug').notNull(),
|
|
name: text('name').notNull(),
|
|
country: text('country'),
|
|
modelCount: integer('model_count').notNull().default(0),
|
|
nlCount: integer('nl_count').notNull().default(0),
|
|
firstYear: smallint('first_year'),
|
|
lastYear: smallint('last_year'),
|
|
sources: text('sources').array().notNull().default([]),
|
|
/** Wikidata item of the brand/manufacturer and its enwiki article (scripts/cars-photos.ts). */
|
|
wikidata: text('wikidata'),
|
|
wikipedia: text('wikipedia'),
|
|
/** 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 brand's most common model. */
|
|
photo: text('photo'),
|
|
...timestamps
|
|
},
|
|
(t) => [uniqueIndex('brands_slug_key').on(t.slug)]
|
|
);
|
|
|
|
export const carModels = carsSchema.table(
|
|
'models',
|
|
{
|
|
id: id(),
|
|
dex: integer('dex').notNull(),
|
|
brandId: integer('brand_id')
|
|
.notNull()
|
|
.references(() => carBrands.id, { onDelete: 'cascade' }),
|
|
slug: text('slug').notNull(),
|
|
name: text('name').notNull(),
|
|
/** Lowercase brand + name + trade names, for ILIKE / trigram search. */
|
|
search: text('search').notNull(),
|
|
firstYear: smallint('first_year'),
|
|
lastYear: smallint('last_year'),
|
|
/** Consumer body types: hatchback, sedan, wagon, suv, mpv, van, pickup, coupe, convertible, sports. */
|
|
bodies: text('bodies').array().notNull().default([]),
|
|
/** US EPA size classes (e.g. "Compact Cars", "Small SUV"). */
|
|
sizeClasses: text('size_classes').array().notNull().default([]),
|
|
/** petrol, diesel, hybrid, plug-in hybrid, electric, lpg, cng, hydrogen. */
|
|
fuels: text('fuels').array().notNull().default([]),
|
|
/** fwd, rwd, awd, 4wd. */
|
|
drives: text('drives').array().notNull().default([]),
|
|
transmissions: text('transmissions').array().notNull().default([]),
|
|
engineLMin: real('engine_l_min'),
|
|
engineLMax: real('engine_l_max'),
|
|
cylindersMin: smallint('cylinders_min'),
|
|
cylindersMax: smallint('cylinders_max'),
|
|
turbo: boolean('turbo'),
|
|
powerKwMin: real('power_kw_min'),
|
|
powerKwMax: real('power_kw_max'),
|
|
/** Combined consumption, litres per 100 km (RDW type approval, NEDC or WLTP). */
|
|
l100Min: real('l100_min'),
|
|
l100Max: real('l100_max'),
|
|
co2Min: smallint('co2_min'), // g/km (RDW)
|
|
co2Max: smallint('co2_max'),
|
|
mpgMin: real('mpg_min'), // combined (EPA; MPGe for EVs)
|
|
mpgMax: real('mpg_max'),
|
|
rangeKmMax: smallint('range_km_max'), // EV range (EPA)
|
|
seats: smallint('seats'),
|
|
doors: smallint('doors'),
|
|
massKg: smallint('mass_kg'), // NL avg kerb weight (massa rijklaar)
|
|
nlCount: integer('nl_count').notNull().default(0),
|
|
nlRank: integer('nl_rank'),
|
|
nlFirstYear: smallint('nl_first_year'),
|
|
nlLastYear: smallint('nl_last_year'),
|
|
nlPrice: integer('nl_price'), // avg catalogue price (EUR, incl. taxes) over all NL registrations
|
|
nlPriceLatest: integer('nl_price_latest'), // avg catalogue price in the latest year with data
|
|
nlPriceLatestYear: smallint('nl_price_latest_year'),
|
|
nlYears: jsonb('nl_years').$type<CarNlYear[]>().notNull().default([]),
|
|
nlNames: jsonb('nl_names').$type<CarNlName[]>().notNull().default([]),
|
|
usYears: smallint('us_years').array().notNull().default([]),
|
|
variantCount: integer('variant_count').notNull().default(0),
|
|
sources: text('sources').array().notNull().default([]),
|
|
/** Matched Wikidata item / enwiki article (scripts/cars-photos.ts); null = no confident match. */
|
|
wikidata: text('wikidata'),
|
|
wikipedia: text('wikipedia'),
|
|
/** Lead photo thumbnail URL (960px), denormalised from cars.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('models_dex_key').on(t.dex),
|
|
uniqueIndex('models_brand_slug_key').on(t.brandId, t.slug),
|
|
index('models_nl_count_idx').on(t.nlCount),
|
|
index('models_search_trgm_idx').using('gin', sql`${t.search} public.gin_trgm_ops`)
|
|
]
|
|
);
|
|
|
|
/** One row per fueleconomy.gov test vehicle (model-year x engine x transmission x drive). */
|
|
export const carVariants = carsSchema.table(
|
|
'variants',
|
|
{
|
|
id: id(),
|
|
modelId: integer('model_id')
|
|
.notNull()
|
|
.references(() => carModels.id, { onDelete: 'cascade' }),
|
|
source: text('source').notNull().default('fueleconomy.gov'),
|
|
sourceId: integer('source_id'),
|
|
year: smallint('year').notNull(),
|
|
name: text('name').notNull(),
|
|
engineL: real('engine_l'),
|
|
cylinders: smallint('cylinders'),
|
|
transmission: text('transmission'),
|
|
drive: text('drive'),
|
|
fuel: text('fuel'),
|
|
sizeClass: text('size_class'),
|
|
mpgCity: real('mpg_city'),
|
|
mpgHwy: real('mpg_hwy'),
|
|
mpgComb: real('mpg_comb'),
|
|
co2Gpm: smallint('co2_gpm'), // tailpipe g/mile
|
|
rangeMi: smallint('range_mi'),
|
|
evMotor: text('ev_motor'),
|
|
turbo: boolean('turbo').notNull().default(false),
|
|
supercharged: boolean('supercharged').notNull().default(false)
|
|
},
|
|
(t) => [index('variants_model_year_idx').on(t.modelId, t.year)]
|
|
);
|
|
|
|
/** Freely licensed photos from Wikimedia Commons; only thumbnail URLs are stored, nothing is downloaded. */
|
|
export const carPhotos = carsSchema.table(
|
|
'photos',
|
|
{
|
|
id: id(),
|
|
modelId: integer('model_id')
|
|
.notNull()
|
|
.references(() => carModels.id, { onDelete: 'cascade' }),
|
|
position: smallint('position').notNull(),
|
|
/** lead (Wikidata image), generation, article (enwiki), commons (Commons category). */
|
|
kind: text('kind').notNull(),
|
|
/** Commons file name without the "File:" prefix. */
|
|
file: text('file').notNull(),
|
|
caption: text('caption'),
|
|
/** 960px thumbnail on upload.wikimedia.org; other standard widths are derived from it. */
|
|
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_model_position_key').on(t.modelId, t.position)]
|
|
);
|