stats step 3: elections, party positions and opinion feeds (schema, collectors, timers); world-opinions data moved into stats.wo_*
All checks were successful
Deploy production / deploy (push) Successful in 1m12s
All checks were successful
Deploy production / deploy (push) Successful in 1m12s
This commit is contained in:
@@ -467,3 +467,280 @@ export const statsCwCbcsBop = statsSchema.table(
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.period, t.item] })]
|
||||
);
|
||||
|
||||
// ── Step 3: elections (A14) ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** One election: NL Tweede Kamer (Kiesraad, CBS 37278) or US presidential (MIT Election Lab). */
|
||||
export const statsElecElection = statsSchema.table('elec_election', {
|
||||
/** NL-TK-2025, US-PRES-2024, … */
|
||||
electionId: text('election_id').primaryKey(),
|
||||
/** ISO 3166-1 alpha-3. */
|
||||
country: char('country', { length: 3 }).notNull(),
|
||||
/** TK (Tweede Kamer), PRES (US president). */
|
||||
kind: text('kind').notNull(),
|
||||
date: date('date', { mode: 'string' }).notNull(),
|
||||
name: text('name').notNull(),
|
||||
sourceId: text('source_id').notNull(),
|
||||
totalSeats: integer('total_seats')
|
||||
});
|
||||
|
||||
export const statsElecResult = statsSchema.table(
|
||||
'elec_result',
|
||||
{
|
||||
electionId: text('election_id').notNull(),
|
||||
/** national, province (kieskring not used), muni, station, state. */
|
||||
level: text('level').notNull(),
|
||||
/** NL / GM0513 / GM0513-SB101 / TX … */
|
||||
areaCode: text('area_code').notNull(),
|
||||
areaName: text('area_name'),
|
||||
/** List name as the source spells it (normalised to a short label on the page). */
|
||||
party: text('party').notNull(),
|
||||
votes: bigint('votes', { mode: 'number' }),
|
||||
seats: integer('seats')
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.electionId, t.level, t.areaCode, t.party] }),
|
||||
index('elec_result_area_idx').on(t.level, t.areaCode)
|
||||
]
|
||||
);
|
||||
|
||||
export const statsElecTurnout = statsSchema.table(
|
||||
'elec_turnout',
|
||||
{
|
||||
electionId: text('election_id').notNull(),
|
||||
level: text('level').notNull(),
|
||||
areaCode: text('area_code').notNull(),
|
||||
areaName: text('area_name'),
|
||||
/** Persons entitled to vote (kiesgerechtigden). */
|
||||
electorate: bigint('electorate', { mode: 'number' }),
|
||||
/** Ballots cast / counted (opkomst). */
|
||||
ballots: bigint('ballots', { mode: 'number' }),
|
||||
valid: bigint('valid', { mode: 'number' }),
|
||||
blank: bigint('blank', { mode: 'number' }),
|
||||
invalid: bigint('invalid', { mode: 'number' })
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.electionId, t.level, t.areaCode] })]
|
||||
);
|
||||
|
||||
// ── Step 3: world opinions (§4, A15) ─────────────────────────────────────────────────────
|
||||
// The /projects/world-opinions guide reads these. Curated rows (seeded from the former
|
||||
// src/lib/world-opinions/data.ts) keep Stan's text exactly; collectors add CHES / V-Party data.
|
||||
|
||||
export const statsWoCountry = statsSchema.table('wo_country', {
|
||||
iso3: char('iso3', { length: 3 }).primaryKey(),
|
||||
/** ISO 3166-1 alpha-2, lower case (the guide's own id). */
|
||||
id: text('id').notNull().unique(),
|
||||
/** ISO 3166-1 numeric as a zero-padded string (world-atlas shape id). */
|
||||
isoNum: text('iso_num').notNull(),
|
||||
name: text('name').notNull(),
|
||||
/** One of the 11 RegionId keys in src/lib/world-opinions/data.ts. */
|
||||
region: text('region').notNull(),
|
||||
/** left | market | western */
|
||||
liberalMeaning: text('liberal_meaning'),
|
||||
/** economy | culture | history | none */
|
||||
mainDivide: text('main_divide'),
|
||||
note: text('note'),
|
||||
/** Country profile {center, fights[], timeline[{year,label}], blindSpots[]}, or null. */
|
||||
profile: jsonb('profile'),
|
||||
sort: integer('sort').notNull()
|
||||
});
|
||||
|
||||
/** Inglehart–Welzel map positions; the guide's hand-placed values are wave 'curated'. */
|
||||
export const statsWoCulturalMap = statsSchema.table(
|
||||
'wo_cultural_map',
|
||||
{
|
||||
iso3: char('iso3', { length: 3 }).notNull(),
|
||||
wave: text('wave').notNull(),
|
||||
yearFrom: integer('year_from'),
|
||||
yearTo: integer('year_to'),
|
||||
/** y axis: traditional (-) ↔ secular-rational (+). */
|
||||
tradSecular: doublePrecision('trad_secular'),
|
||||
/** x axis: survival (-) ↔ self-expression (+). */
|
||||
survSelfexp: doublePrecision('surv_selfexp'),
|
||||
n: integer('n'),
|
||||
source: text('source')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.iso3, t.wave] })]
|
||||
);
|
||||
|
||||
/** WVS/EVS/Pew item aggregates (Tier C manual loads; empty until then). */
|
||||
export const statsWoValueScore = statsSchema.table(
|
||||
'wo_value_score',
|
||||
{
|
||||
iso3: char('iso3', { length: 3 }).notNull(),
|
||||
wave: text('wave').notNull(),
|
||||
itemCode: text('item_code').notNull(),
|
||||
itemLabel: text('item_label'),
|
||||
stat: text('stat').notNull(),
|
||||
value: doublePrecision('value'),
|
||||
n: integer('n')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.iso3, t.wave, t.itemCode, t.stat] })]
|
||||
);
|
||||
|
||||
export const statsWoNarrative = statsSchema.table('wo_narrative', {
|
||||
sort: integer('sort').primaryKey(),
|
||||
event: text('event').notNull(),
|
||||
years: text('years').notNull(),
|
||||
/** [{country (alpha-2), story}] in display order. */
|
||||
views: jsonb('views').notNull()
|
||||
});
|
||||
|
||||
export const statsWoSource = statsSchema.table('wo_source', {
|
||||
sort: integer('sort').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
href: text('href').notNull()
|
||||
});
|
||||
|
||||
/**
|
||||
* Parties. Curated guide parties: party_id `wo:<cc>:<short>`; data-driven parties:
|
||||
* `pf:<partyfacts_id>` (or `ches:<id>` / `vp:<id>` when Party Facts has no link).
|
||||
*/
|
||||
export const statsWoParty = statsSchema.table(
|
||||
'wo_party',
|
||||
{
|
||||
partyId: text('party_id').primaryKey(),
|
||||
iso3: char('iso3', { length: 3 }),
|
||||
name: text('name'),
|
||||
abbrev: text('abbrev'),
|
||||
family: text('family'),
|
||||
chesId: integer('ches_id'),
|
||||
manifestoId: integer('manifesto_id'),
|
||||
vpartyId: integer('vparty_id'),
|
||||
partyfactsId: integer('partyfacts_id'),
|
||||
/** Display order of curated parties (null for data-driven rows). */
|
||||
sort: integer('sort')
|
||||
},
|
||||
(t) => [index('wo_party_iso3_idx').on(t.iso3)]
|
||||
);
|
||||
|
||||
export const statsWoPartyPosition = statsSchema.table(
|
||||
'wo_party_position',
|
||||
{
|
||||
partyId: text('party_id').notNull(),
|
||||
year: integer('year').notNull(),
|
||||
/** ches, vparty */
|
||||
source: text('source').notNull(),
|
||||
/** lrgen, lrecon, galtan, eu_position, v2xpa_popul, v2pariglef, … */
|
||||
dim: text('dim').notNull(),
|
||||
value: doublePrecision('value'),
|
||||
voteShare: doublePrecision('vote_share')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.partyId, t.year, t.source, t.dim] })]
|
||||
);
|
||||
|
||||
/** Stan's curated shared-compass placements (x = economic 0–10, y = social 0–10). */
|
||||
export const statsWoCompassOverride = statsSchema.table('wo_compass_override', {
|
||||
partyId: text('party_id').primaryKey(),
|
||||
x: doublePrecision('x').notNull(),
|
||||
y: doublePrecision('y').notNull(),
|
||||
note: text('note')
|
||||
});
|
||||
|
||||
// ── Step 3: opinion feeds (A16) ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Raw Wikipedia wikitext of the poll pages, one row per revision seen. */
|
||||
export const statsOpWikiSnapshot = statsSchema.table(
|
||||
'op_wiki_snapshot',
|
||||
{
|
||||
page: text('page').notNull(),
|
||||
revid: bigint('revid', { mode: 'number' }).notNull(),
|
||||
fetchedAt: timestamptz('fetched_at').notNull().defaultNow(),
|
||||
wikitext: text('wikitext').notNull()
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.page, t.revid] })]
|
||||
);
|
||||
|
||||
/** Parsed poll rows (long format): one value per poll × option. */
|
||||
export const statsOpPoll = statsSchema.table(
|
||||
'op_poll',
|
||||
{
|
||||
/** nl-tk (seats), us-approval, us-approval-avg, us-generic, kr-approval, kr-party. */
|
||||
series: text('series').notNull(),
|
||||
/** `${fieldwork_end}|${pollster}|${n}` (n = order within that pollster and date). */
|
||||
pollKey: text('poll_key').notNull(),
|
||||
fieldworkEnd: date('fieldwork_end', { mode: 'string' }).notNull(),
|
||||
pollster: text('pollster').notNull(),
|
||||
sample: integer('sample'),
|
||||
option: text('option').notNull(),
|
||||
value: doublePrecision('value'),
|
||||
seenAt: timestamptz('seen_at').notNull().defaultNow()
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.series, t.pollKey, t.option] }),
|
||||
index('op_poll_series_date_idx').on(t.series, t.fieldworkEnd)
|
||||
]
|
||||
);
|
||||
|
||||
/** Standard Eurobarometer volume A: most-important-issue and trust questions per country. */
|
||||
export const statsEbItem = statsSchema.table(
|
||||
'eb_item',
|
||||
{
|
||||
/** 104.1 etc. */
|
||||
wave: text('wave').notNull(),
|
||||
fieldworkEnd: date('fieldwork_end', { mode: 'string' }),
|
||||
/** mii_country, mii_personal, mii_eu, trust */
|
||||
topic: text('topic').notNull(),
|
||||
/** Sheet name in the wave's file (QA3, QA6_9, …). */
|
||||
qcode: text('qcode').notNull(),
|
||||
/** Answer (issue) for MII; institution for trust. */
|
||||
item: text('item').notNull(),
|
||||
/** Eurobarometer country code (EU27, BE, EL, …). */
|
||||
country: text('country').notNull(),
|
||||
/** Share of respondents, 0–1. */
|
||||
share: doublePrecision('share')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.wave, t.topic, t.item, t.country] })]
|
||||
);
|
||||
|
||||
/** World Happiness Report: life evaluation (3-year average) and its explained parts. */
|
||||
export const statsWhrScore = statsSchema.table(
|
||||
'whr_score',
|
||||
{
|
||||
year: integer('year').notNull(),
|
||||
country: text('country').notNull(),
|
||||
iso3: char('iso3', { length: 3 }),
|
||||
rank: integer('rank'),
|
||||
ladder: doublePrecision('ladder'),
|
||||
lower: doublePrecision('lower'),
|
||||
upper: doublePrecision('upper'),
|
||||
gdp: doublePrecision('gdp'),
|
||||
social: doublePrecision('social'),
|
||||
health: doublePrecision('health'),
|
||||
freedom: doublePrecision('freedom'),
|
||||
generosity: doublePrecision('generosity'),
|
||||
corruption: doublePrecision('corruption'),
|
||||
dystopia: doublePrecision('dystopia')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.year, t.country] })]
|
||||
);
|
||||
|
||||
/** Google Trends daily trending searches (RSS snapshot). */
|
||||
export const statsGtTrending = statsSchema.table(
|
||||
'gt_trending',
|
||||
{
|
||||
geo: text('geo').notNull(),
|
||||
date: date('date', { mode: 'string' }).notNull(),
|
||||
rank: integer('rank').notNull(),
|
||||
query: text('query').notNull(),
|
||||
approxTraffic: integer('approx_traffic'),
|
||||
pubDate: timestamptz('pub_date'),
|
||||
/** First related news headline + source, for context. */
|
||||
news: text('news')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.geo, t.date, t.query] })]
|
||||
);
|
||||
|
||||
/** GDELT DOC timelinevol: share of monitored news coverage per theme and source country. */
|
||||
export const statsGdeltThemeVol = statsSchema.table(
|
||||
'gdelt_theme_vol',
|
||||
{
|
||||
date: date('date', { mode: 'string' }).notNull(),
|
||||
/** ISO 3166-1 alpha-3 of the source country. */
|
||||
country: char('country', { length: 3 }).notNull(),
|
||||
theme: text('theme').notNull(),
|
||||
/** Percentage of all monitored articles (daily mean of GDELT's volume intensity). */
|
||||
volPct: doublePrecision('vol_pct')
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.date, t.country, t.theme] })]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user