planes: schema and migration
This commit is contained in:
@@ -26,7 +26,8 @@ export default defineConfig({
|
||||
'home',
|
||||
'cars',
|
||||
'music',
|
||||
'hardware'
|
||||
'hardware',
|
||||
'planes'
|
||||
],
|
||||
migrations: { schema: 'drizzle', table: '__drizzle_migrations' },
|
||||
verbose: true,
|
||||
|
||||
140
drizzle/20261024193304_planes/migration.sql
Normal file
140
drizzle/20261024193304_planes/migration.sql
Normal file
@@ -0,0 +1,140 @@
|
||||
CREATE SCHEMA "planes";
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "planes"."makers" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"slug" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"full_name" text,
|
||||
"country" text,
|
||||
"wikidata" text,
|
||||
"wikipedia" text,
|
||||
"type_count" integer DEFAULT 0 NOT NULL,
|
||||
"first_year" smallint,
|
||||
"last_year" smallint,
|
||||
"built_total" integer DEFAULT 0 NOT NULL,
|
||||
"sources" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
"logo" text,
|
||||
"logo_file" text,
|
||||
"logo_author" text,
|
||||
"logo_license" text,
|
||||
"photo" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "planes"."photos" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"type_id" integer NOT NULL,
|
||||
"position" smallint NOT NULL,
|
||||
"kind" text NOT NULL,
|
||||
"file" text NOT NULL,
|
||||
"caption" text,
|
||||
"thumb" text NOT NULL,
|
||||
"width" integer NOT NULL,
|
||||
"height" integer NOT NULL,
|
||||
"author" text,
|
||||
"license" text,
|
||||
"license_url" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "planes"."types" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"dex" integer NOT NULL,
|
||||
"maker_id" integer NOT NULL,
|
||||
"family_id" integer,
|
||||
"slug" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"full_name" text NOT NULL,
|
||||
"search" text NOT NULL,
|
||||
"wikidata" text NOT NULL,
|
||||
"wikipedia" text,
|
||||
"kind" text,
|
||||
"description" text,
|
||||
"summary" text,
|
||||
"role" text,
|
||||
"roles" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
"role_label" text,
|
||||
"country" text,
|
||||
"first_flight" text,
|
||||
"first_flight_year" smallint,
|
||||
"introduced_year" smallint,
|
||||
"retired_year" smallint,
|
||||
"status" text,
|
||||
"in_service" boolean,
|
||||
"number_built" integer,
|
||||
"makers" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
"engine_type" text,
|
||||
"engine_count" smallint,
|
||||
"engines" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
"crew" smallint,
|
||||
"passengers" smallint,
|
||||
"length_m" real,
|
||||
"wingspan_m" real,
|
||||
"height_m" real,
|
||||
"rotor_m" real,
|
||||
"mtow_kg" integer,
|
||||
"max_speed_kmh" smallint,
|
||||
"cruise_kmh" smallint,
|
||||
"range_km" integer,
|
||||
"ceiling_m" integer,
|
||||
"wing" text,
|
||||
"gear" text,
|
||||
"icao" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
"recognize" text,
|
||||
"sources" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
"photo" text,
|
||||
"photo_count" smallint DEFAULT 0 NOT NULL,
|
||||
"photos_checked_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "makers_slug_key" ON "planes"."makers" ("slug");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "photos_type_position_key" ON "planes"."photos" ("type_id","position");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "types_dex_key" ON "planes"."types" ("dex");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "types_wikidata_key" ON "planes"."types" ("wikidata");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "types_maker_slug_key" ON "planes"."types" ("maker_id","slug");--> statement-breakpoint
|
||||
CREATE INDEX "types_number_built_idx" ON "planes"."types" ("number_built");--> statement-breakpoint
|
||||
CREATE INDEX "types_first_flight_year_idx" ON "planes"."types" ("first_flight_year");--> statement-breakpoint
|
||||
CREATE INDEX "types_search_trgm_idx" ON "planes"."types" USING gin ("search" public.gin_trgm_ops);--> statement-breakpoint
|
||||
ALTER TABLE "planes"."photos" ADD CONSTRAINT "photos_type_id_types_id_fkey" FOREIGN KEY ("type_id") REFERENCES "planes"."types"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "planes"."types" ADD CONSTRAINT "types_maker_id_makers_id_fkey" FOREIGN KEY ("maker_id") REFERENCES "planes"."makers"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "planes"."types" ADD CONSTRAINT "types_family_id_types_id_fkey" FOREIGN KEY ("family_id") REFERENCES "planes"."types"("id") ON DELETE SET NULL;--> statement-breakpoint
|
||||
-- Hand-written: updated_at triggers, grants, GUI registration. Data comes from scripts/planes-import.ts and scripts/planes-photos.ts.
|
||||
DO $$
|
||||
DECLARE t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['makers','types'] LOOP
|
||||
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON planes.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', t);
|
||||
END LOOP;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN
|
||||
GRANT USAGE ON SCHEMA planes TO personal_app;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA planes TO personal_app;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA planes TO personal_app;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA planes GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA planes GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_ro') THEN
|
||||
GRANT USAGE ON SCHEMA planes TO personal_ro;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA planes TO personal_ro;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA planes GRANT SELECT ON TABLES TO personal_ro;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('meta.tables') IS NOT NULL THEN
|
||||
INSERT INTO meta.tables (schema_name, table_name, slug, display_name, kind, editable, has_done, sort_order)
|
||||
VALUES ('planes','makers','planes-makers','Plane makers','core',false,false,1000),
|
||||
('planes','types','planes-types','Plane types','core',false,false,1000),
|
||||
('planes','photos','planes-photos','Plane photos','core',false,false,1000)
|
||||
ON CONFLICT DO NOTHING;
|
||||
IF has_function_privilege('meta.sync(integer)', 'EXECUTE') THEN
|
||||
PERFORM meta.sync();
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
22615
drizzle/20261024193304_planes/snapshot.json
Normal file
22615
drizzle/20261024193304_planes/snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,5 +18,6 @@ export * from './tasks';
|
||||
export * from './groceries';
|
||||
export * from './home';
|
||||
export * from './cars';
|
||||
export * from './planes';
|
||||
export * from './music';
|
||||
export * from './hardware';
|
||||
|
||||
157
src/lib/server/db/schema/planes.ts
Normal file
157
src/lib/server/db/schema/planes.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
real,
|
||||
smallint,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
type AnyPgColumn
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { planesSchema } from './schemas';
|
||||
import { id, timestamps } from './helpers';
|
||||
|
||||
// Public plane "pokedex" (/trackers/planes). Read-only catalogue built from open data by
|
||||
// scripts/planes-import.ts (re-runnable; upserts types by Wikidata id so Dex numbers stay
|
||||
// stable) and scripts/planes-photos.ts (Commons photos + maker logos). Sources:
|
||||
// - Wikidata (CC0): aircraft families/models with an English Wikipedia article.
|
||||
// - English Wikipedia (CC BY-SA): infobox status/type/number built, the specifications
|
||||
// block (dimensions, MTOW, speed, range, ceiling, crew, capacity, engines) and the intro,
|
||||
// plus ICAO type designators from "List of aircraft type designators".
|
||||
// Only production types (no prototypes, drones or one-offs). Unknown stays null.
|
||||
|
||||
export const planeMakers = planesSchema.table(
|
||||
'makers',
|
||||
{
|
||||
id: id(),
|
||||
slug: text('slug').notNull(),
|
||||
/** Short name as used in article titles ("Cessna"); full_name is the Wikidata label. */
|
||||
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 top-level 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 planeTypes = planesSchema.table(
|
||||
'types',
|
||||
{
|
||||
id: id(),
|
||||
dex: integer('dex').notNull(),
|
||||
makerId: integer('maker_id')
|
||||
.notNull()
|
||||
.references(() => planeMakers.id, { onDelete: 'cascade' }),
|
||||
/** Parent type in the dex (e.g. 737 MAX -> 737), from Wikidata "subclass of". */
|
||||
familyId: integer('family_id').references((): AnyPgColumn => planeTypes.id, {
|
||||
onDelete: 'set null'
|
||||
}),
|
||||
slug: text('slug').notNull(),
|
||||
/** Name without the maker ("737"), full_name is the article title ("Boeing 737"). */
|
||||
name: text('name').notNull(),
|
||||
fullName: text('full_name').notNull(),
|
||||
/** Lowercase maker + names + aliases + ICAO codes, for ILIKE / trigram search. */
|
||||
search: text('search').notNull(),
|
||||
wikidata: text('wikidata').notNull(),
|
||||
wikipedia: text('wikipedia'),
|
||||
/** Wikidata class: family, model or type. */
|
||||
kind: text('kind'),
|
||||
description: text('description'),
|
||||
/** First sentences of the Wikipedia article. */
|
||||
summary: text('summary'),
|
||||
/** Primary role: airliner, regional, cargo, business, general aviation, glider, helicopter, military, airship. */
|
||||
role: text('role'),
|
||||
roles: text('roles').array().notNull().default([]),
|
||||
/** Infobox "type" (e.g. "Narrow-body airliner"). */
|
||||
roleLabel: text('role_label'),
|
||||
country: text('country'),
|
||||
/** ISO date trimmed to its precision (YYYY, YYYY-MM or YYYY-MM-DD). */
|
||||
firstFlight: text('first_flight'),
|
||||
firstFlightYear: smallint('first_flight_year'),
|
||||
introducedYear: smallint('introduced_year'),
|
||||
retiredYear: smallint('retired_year'),
|
||||
/** in production, in service, in development, retired. */
|
||||
status: text('status'),
|
||||
inService: boolean('in_service'),
|
||||
numberBuilt: integer('number_built'),
|
||||
/** All manufacturers (Wikidata labels). */
|
||||
makers: text('makers').array().notNull().default([]),
|
||||
/** jet, turboprop, piston, turboshaft, electric, rocket, none. */
|
||||
engineType: text('engine_type'),
|
||||
engineCount: smallint('engine_count'),
|
||||
engines: text('engines').array().notNull().default([]),
|
||||
crew: smallint('crew'),
|
||||
passengers: smallint('passengers'),
|
||||
lengthM: real('length_m'),
|
||||
wingspanM: real('wingspan_m'),
|
||||
heightM: real('height_m'),
|
||||
rotorM: real('rotor_m'),
|
||||
mtowKg: integer('mtow_kg'),
|
||||
maxSpeedKmh: smallint('max_speed_kmh'),
|
||||
cruiseKmh: smallint('cruise_kmh'),
|
||||
rangeKm: integer('range_km'),
|
||||
ceilingM: integer('ceiling_m'),
|
||||
/** Wikidata wing configuration / undercarriage labels. */
|
||||
wing: text('wing'),
|
||||
gear: text('gear'),
|
||||
icao: text('icao').array().notNull().default([]),
|
||||
/** "How to recognise it", derived from engines, wings, gear and size. */
|
||||
recognize: text('recognize'),
|
||||
sources: text('sources').array().notNull().default([]),
|
||||
/** Lead photo thumbnail URL (960px), denormalised from planes.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_flight_year_idx').on(t.firstFlightYear),
|
||||
index('types_search_trgm_idx').using('gin', sql`${t.search} public.gin_trgm_ops`)
|
||||
]
|
||||
);
|
||||
|
||||
/** Freely licensed photos from Wikimedia Commons; only thumbnail URLs are stored, nothing is downloaded. */
|
||||
export const planePhotos = planesSchema.table(
|
||||
'photos',
|
||||
{
|
||||
id: id(),
|
||||
typeId: integer('type_id')
|
||||
.notNull()
|
||||
.references(() => planeTypes.id, { onDelete: 'cascade' }),
|
||||
position: smallint('position').notNull(),
|
||||
/** lead (Wikidata image), variant (sub-type image), 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_type_position_key').on(t.typeId, t.position)]
|
||||
);
|
||||
@@ -20,3 +20,4 @@ export const homeSchema = pgSchema('home');
|
||||
export const carsSchema = pgSchema('cars');
|
||||
export const musicSchema = pgSchema('music');
|
||||
export const hardwareSchema = pgSchema('hardware');
|
||||
export const planesSchema = pgSchema('planes');
|
||||
|
||||
Reference in New Issue
Block a user