@finbheara/archive-client 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -201,6 +201,7 @@ var feisesIndex = {
201
201
  }
202
202
  };
203
203
  var NO_CALENDAR2 = "the release holds a feis-year once its results are captured and carries no calendar of scheduled feiseanna";
204
+ var NO_UPCOMING2 = "the release's feis-index lists no occurrence of the series dated on or after the cut's day";
204
205
  var globToLike = (g) => g.replace(/[\\%_]/g, (c) => `\\${c}`).replace(/\*/g, "%").replace(/\?/g, "_");
205
206
 
206
207
  class FeisFinder2 {
@@ -296,6 +297,29 @@ class FeisFinder2 {
296
297
  instances
297
298
  };
298
299
  }
300
+ async nextOf(brand) {
301
+ if ((await this.rel.capability("upcoming.feises")).state === "missing")
302
+ return { next: null, next_why: NO_CALENDAR2 };
303
+ const db = await this.rel.indexDb("feises");
304
+ const retired = db.query("SELECT old_id FROM redirect WHERE new_id = ? AND kind = 'brand' ORDER BY old_id").all(brand).map((r) => r.old_id);
305
+ let best = null;
306
+ for (const id of [brand, ...retired])
307
+ for (const o of (await this.rel.upcoming.feises({ state: "upcoming", brand: id })).feises) {
308
+ if (best !== null && (best.date < o.date_lo || best.date === o.date_lo && best.occurrence_id <= o.occurrence_id))
309
+ continue;
310
+ best = {
311
+ feis_id: o.hosts.find((h) => h.feis_id !== null)?.feis_id ?? null,
312
+ date: o.date_lo,
313
+ date_hi: o.date_hi,
314
+ occurrence_id: o.occurrence_id,
315
+ name_printed: o.name_printed,
316
+ hosts: o.hosts,
317
+ captured_id: o.captured_id,
318
+ series_basis: o.series_basis
319
+ };
320
+ }
321
+ return best === null ? { next: null, next_why: NO_UPCOMING2 } : { next: best };
322
+ }
299
323
  async series(q) {
300
324
  const asked = q.trim();
301
325
  const answer = async (brands, via, more = {}) => {
@@ -314,8 +338,7 @@ class FeisFinder2 {
314
338
  host: u?.host ?? null,
315
339
  years: u?.years ?? [],
316
340
  instances: u?.instances ?? [],
317
- next: null,
318
- ...u !== null ? { next_why: NO_CALENDAR2 } : {},
341
+ ...u !== null ? await this.nextOf(u.series_id) : { next: null },
319
342
  candidates,
320
343
  ...arm === "ambiguous" ? { why: `${candidates.length} series answer "${asked}": ${brands.join(", ")}` } : {},
321
344
  ...more
@@ -774,6 +797,96 @@ import {
774
797
  DEFAULT_SCHOOL_NAME_READER,
775
798
  schoolEncoder
776
799
  } from "@finbheara/domain/school";
800
+
801
+ var str = (v) => typeof v === "string" && v !== "" ? v : null;
802
+ var num = (v) => typeof v === "number" ? v : null;
803
+ var uniq = (xs) => [...new Set(xs)];
804
+ async function registrySchools(rel) {
805
+ await rel.require("schools.registry");
806
+ await rel.fetch("school-registry.jsonl");
807
+ const schools = readJsonl(rel.path("school-registry.jsonl")).map((r) => ({
808
+ school_id: r.school_id,
809
+ display: r.display,
810
+ regions: uniq((r.regions ?? []).map((x) => x.region)),
811
+ places: uniq((r.locations ?? []).map((x) => x.place))
812
+ })).sort((a, b) => a.school_id < b.school_id ? -1 : a.school_id > b.school_id ? 1 : 0);
813
+ return { tag: rel.tag, schools };
814
+ }
815
+ async function schoolPrints(rel, q) {
816
+ await rel.fetch("results");
817
+ await rel.require("schools.prints");
818
+ const where = ["r.school_id IS NOT NULL"];
819
+ const args = [];
820
+ if (q.event !== undefined) {
821
+ where.push("c.event = ? COLLATE NOCASE");
822
+ args.push(q.event);
823
+ }
824
+ if (q.year !== undefined) {
825
+ where.push("c.year = ?");
826
+ args.push(q.year);
827
+ }
828
+ const prints = rel.db("results").query(`SELECT c.event AS event, c.year AS year, trim(r.school_printed) AS print,
829
+ r.school_id AS school_id, count(*) AS rows
830
+ FROM competitions c JOIN results r ON r.file = c.file AND r.competition_line = c.line
831
+ WHERE ${where.join(" AND ")} GROUP BY 1, 2, 3, 4 ORDER BY 2, 1, 3, 4`).all(...args);
832
+ return { tag: rel.tag, asked: q, prints };
833
+ }
834
+ async function schoolRedirects(rel) {
835
+ await rel.require("schools.redirects");
836
+ await rel.fetch("school-redirects.jsonl");
837
+ const redirects = readJsonl(rel.path("school-redirects.jsonl")).map((r) => ({
838
+ old_id: String(r.old_id),
839
+ new_id: String(r.new_id),
840
+ kind: String(r.kind),
841
+ since: num(r.since)
842
+ }));
843
+ return { tag: rel.tag, redirects };
844
+ }
845
+ async function schoolLineage(rel) {
846
+ await rel.require("schools.lineage");
847
+ await rel.fetch("school-lineage.jsonl");
848
+ const edges = readJsonl(rel.path("school-lineage.jsonl")).map((r) => ({
849
+ id: String(r.id),
850
+ direction: String(r.direction),
851
+ other: String(r.other),
852
+ shape: String(r.shape),
853
+ year: num(r.year),
854
+ provisional: r.provisional === true,
855
+ decision: str(r.decision),
856
+ confidence: num(r.confidence)
857
+ }));
858
+ return { tag: rel.tag, edges };
859
+ }
860
+ async function observedAssociations(rel, q) {
861
+ await rel.require("schools.associations");
862
+ await rel.fetch("observed-associations.jsonl");
863
+ const associations = [];
864
+ for (const r of readJsonl(rel.path("observed-associations.jsonl"))) {
865
+ const school_id = str(r.schoolId);
866
+ if (q.school_id !== undefined && school_id !== q.school_id)
867
+ continue;
868
+ associations.push({
869
+ at: String(r.at),
870
+ seq: num(r.seq) ?? 0,
871
+ date: str(r.date),
872
+ person: str(r.person),
873
+ person_printed: typeof r.personPrinted === "string" ? r.personPrinted : "",
874
+ reading: str(r.reading),
875
+ person_type: str(r.personType),
876
+ credentials: str(r.credentials),
877
+ caption: str(r.caption),
878
+ school_printed: str(r.school),
879
+ school_id,
880
+ association_type: str(r.associationType),
881
+ src_type: str(r.srcType),
882
+ src: str(r.src),
883
+ candidate_school_id: str(r.candidateSchoolId),
884
+ candidate_rung: str(r.candidateRung)
885
+ });
886
+ }
887
+ return { tag: rel.tag, asked: q, associations };
888
+ }
889
+
777
890
  function readsSpanishNames2(candidates) {
778
891
  return candidates.some((c) => c.spanishNames !== undefined && (c.spanishNames.file || c.prints.some((p) => c.spanishNames?.prints.includes(p.trim()))));
779
892
  }
@@ -846,6 +959,21 @@ class SchoolResolver2 {
846
959
  this.graph ??= releaseSchoolGraph2(this.rel);
847
960
  return { tag: this.rel.tag, ...(await this.graph).line(a.trim(), q) };
848
961
  }
962
+ list() {
963
+ return registrySchools(this.rel);
964
+ }
965
+ prints(q = {}) {
966
+ return schoolPrints(this.rel, q);
967
+ }
968
+ redirects() {
969
+ return schoolRedirects(this.rel);
970
+ }
971
+ lineage() {
972
+ return schoolLineage(this.rel);
973
+ }
974
+ associations(q = {}) {
975
+ return observedAssociations(this.rel, q);
976
+ }
849
977
  async resolve(asked, q = {}) {
850
978
  const db = await this.rel.indexDb("schools");
851
979
  const reg = db.query("SELECT display, regions, places, spanish FROM registry WHERE school_id = ?");
@@ -2246,6 +2374,218 @@ import {
2246
2374
  roleEncoder
2247
2375
  } from "@finbheara/domain/noun-index";
2248
2376
  import { roleName } from "@finbheara/domain/person";
2377
+
2378
+ var TAB_COLUMNS = `address, slug AS competition, card, line, round, true_round, seat_index,
2379
+ section, panel_size, column_order, drop_rule, judge_name, judge_name_printed, judge_clrg,
2380
+ judge_id, judge_unmatched, mark, mark_printed, place, place_printed, tie, points, points_printed`;
2381
+ var TAB_NEEDS = [
2382
+ "address",
2383
+ "slug",
2384
+ "card",
2385
+ "line",
2386
+ "round",
2387
+ "true_round",
2388
+ "seat_index",
2389
+ "section",
2390
+ "panel_size",
2391
+ "column_order",
2392
+ "drop_rule",
2393
+ "judge_name",
2394
+ "judge_name_printed",
2395
+ "judge_clrg",
2396
+ "judge_id",
2397
+ "judge_unmatched",
2398
+ "mark",
2399
+ "mark_printed",
2400
+ "place",
2401
+ "place_printed",
2402
+ "tie",
2403
+ "points",
2404
+ "points_printed"
2405
+ ].map((c) => `results:tabs.${c}`);
2406
+ var NO_SHEET = { event: null, year: null, tier: null };
2407
+ var cardRange = (competition) => [`${competition}#`, `${competition}$`];
2408
+
2409
+ class TabFinder2 {
2410
+ rel;
2411
+ constructor(rel) {
2412
+ this.rel = rel;
2413
+ }
2414
+ async db() {
2415
+ await this.rel.fetch("results");
2416
+ await this.rel.require("tabs");
2417
+ return this.rel.db("results");
2418
+ }
2419
+ async sheets(db, only) {
2420
+ const out = new Map;
2421
+ const tiers = new Map;
2422
+ const rows = [];
2423
+ if (only === null)
2424
+ rows.push(...db.query("SELECT competition, event, year, tier FROM competitions WHERE competition IN (SELECT DISTINCT slug FROM tabs)").all());
2425
+ else
2426
+ for (let i = 0;i < only.length; i += 500) {
2427
+ const part = only.slice(i, i + 500);
2428
+ rows.push(...db.query(`SELECT competition, event, year, tier FROM competitions WHERE competition IN (${part.map(() => "?").join(", ")})`).all(...part));
2429
+ }
2430
+ for (const r of rows) {
2431
+ out.set(r.competition, { event: r.event, year: r.year, tier: r.tier });
2432
+ tiers.set(`${r.event.toLowerCase()}\x00${r.year}`, r.tier);
2433
+ }
2434
+ const missing = only === null ? null : only.filter((c) => !out.has(c));
2435
+ if (missing !== null && missing.length === 0)
2436
+ return out;
2437
+ if ((await this.rel.capability("teams")).state !== "full")
2438
+ return out;
2439
+ const teams = await this.rel.indexDb("teams");
2440
+ const team = teams.query("SELECT competition, event, year FROM result WHERE competition IS NOT NULL GROUP BY competition");
2441
+ const want = missing === null ? null : new Set(missing);
2442
+ for (const t of team.all()) {
2443
+ if (out.has(t.competition) || want !== null && !want.has(t.competition))
2444
+ continue;
2445
+ out.set(t.competition, {
2446
+ event: t.event,
2447
+ year: t.year,
2448
+ tier: tiers.get(`${t.event.toLowerCase()}\x00${t.year}`) ?? null
2449
+ });
2450
+ }
2451
+ if ([...out.values()].some((s) => s.tier === null && s.event !== null)) {
2452
+ for (const r of db.query("SELECT event, year, min(tier) AS tier FROM competitions GROUP BY event, year").all())
2453
+ tiers.set(`${r.event.toLowerCase()}\x00${r.year}`, r.tier);
2454
+ for (const s of out.values())
2455
+ if (s.tier === null && s.event !== null)
2456
+ s.tier = tiers.get(`${s.event.toLowerCase()}\x00${s.year}`) ?? null;
2457
+ }
2458
+ return out;
2459
+ }
2460
+ async query(q) {
2461
+ const db = await this.db();
2462
+ const where = [];
2463
+ const args = [];
2464
+ if (q.address !== undefined) {
2465
+ where.push("address = ?");
2466
+ args.push(q.address);
2467
+ } else if (q.competition !== undefined) {
2468
+ const [lo, hi] = cardRange(q.competition);
2469
+ where.push("address >= ? AND address < ? AND slug = ?");
2470
+ args.push(lo, hi, q.competition);
2471
+ } else
2472
+ throw new Error("archive-client: a tabs query names a competition or an address");
2473
+ if (q.round !== undefined) {
2474
+ where.push("round = ?");
2475
+ args.push(q.round);
2476
+ }
2477
+ const raw = db.query(`SELECT ${TAB_COLUMNS} FROM tabs WHERE ${where.join(" AND ")} ORDER BY address, round, seat_index, line`).all(...args);
2478
+ const sheets = raw.length === 0 ? new Map : await this.sheets(db, [...new Set(raw.map((r) => r.competition))]);
2479
+ return {
2480
+ tag: this.rel.tag,
2481
+ asked: q,
2482
+ rows: raw.map((r) => ({ ...r, ...sheets.get(r.competition) ?? NO_SHEET }))
2483
+ };
2484
+ }
2485
+ async* stream(q = {}) {
2486
+ const db = await this.db();
2487
+ const sheets = await this.sheets(db, q.competition ?? null);
2488
+ const events = q.event === undefined ? null : new Set((typeof q.event === "string" ? [q.event] : q.event).map((e) => e.toLowerCase()));
2489
+ const tiers = q.tier === undefined ? null : new Set(q.tier);
2490
+ const holds = (s) => {
2491
+ if (events !== null && (s.event === null || !events.has(s.event.toLowerCase())))
2492
+ return false;
2493
+ if (tiers !== null && (s.tier === null || !tiers.has(s.tier)))
2494
+ return false;
2495
+ if (typeof q.year === "number" && s.year !== q.year)
2496
+ return false;
2497
+ if (Array.isArray(q.year) && (s.year === null || s.year < q.year[0] || s.year > q.year[1]))
2498
+ return false;
2499
+ return true;
2500
+ };
2501
+ const filtered = events !== null || tiers !== null || q.year !== undefined;
2502
+ const run = function* (stmt, args) {
2503
+ for (const r of stmt.iterate(...args))
2504
+ yield { ...r, ...sheets.get(r.competition) ?? NO_SHEET };
2505
+ };
2506
+ if (!filtered && q.competition === undefined) {
2507
+ const all = db.prepare(`SELECT ${TAB_COLUMNS} FROM tabs ORDER BY address, round, seat_index, line`);
2508
+ try {
2509
+ yield* run(all, []);
2510
+ } finally {
2511
+ all.finalize();
2512
+ }
2513
+ return;
2514
+ }
2515
+ const asked = [...new Set(q.competition ?? sheets.keys())].filter((c) => holds(sheets.get(c) ?? NO_SHEET));
2516
+ const one = db.prepare(`SELECT ${TAB_COLUMNS} FROM tabs WHERE address >= ? AND address < ? AND slug = ? ORDER BY address, round, seat_index, line`);
2517
+ try {
2518
+ for (const c of asked.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
2519
+ const [lo, hi] = cardRange(c);
2520
+ yield* run(one, [lo, hi, c]);
2521
+ }
2522
+ } finally {
2523
+ one.finalize();
2524
+ }
2525
+ }
2526
+ }
2527
+
2528
+ class PanelFinder2 {
2529
+ rel;
2530
+ constructor(rel) {
2531
+ this.rel = rel;
2532
+ }
2533
+ async query(q = {}) {
2534
+ await this.rel.fetch("results");
2535
+ await this.rel.require("panels");
2536
+ const where = q.competition === undefined ? "" : " WHERE slug = ?";
2537
+ const seats = this.rel.db("results").query(`SELECT slug AS competition, panel, round, seat_index, address, judge_name_printed,
2538
+ judge_clrg, judge_id, judge_unmatched
2539
+ FROM panels${where} ORDER BY slug, panel, round, seat_index`).all(...q.competition === undefined ? [] : [q.competition]);
2540
+ return { tag: this.rel.tag, asked: q, seats };
2541
+ }
2542
+ }
2543
+
2544
+ class FeisPanelFinder2 {
2545
+ rel;
2546
+ constructor(rel) {
2547
+ this.rel = rel;
2548
+ }
2549
+ async query(q = {}) {
2550
+ await this.rel.fetch("results");
2551
+ await this.rel.require("feis-panels");
2552
+ const where = [];
2553
+ const args = [];
2554
+ if (q.address !== undefined) {
2555
+ where.push("address = ?");
2556
+ args.push(q.address);
2557
+ }
2558
+ if (q.platform !== undefined) {
2559
+ where.push("platform = ?");
2560
+ args.push(q.platform);
2561
+ }
2562
+ const seats = this.rel.db("results").query(`SELECT address, platform, round, seat_index, panel_size, panel_kind, judge_name_printed,
2563
+ judge_state_printed, judge_clrg, judge_id, judge_unmatched, judge_basis
2564
+ FROM feis_panels${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`}
2565
+ ORDER BY address, round, seat_index`).all(...args);
2566
+ return { tag: this.rel.tag, asked: q, seats };
2567
+ }
2568
+ }
2569
+ async function registerRows(rel, q) {
2570
+ await rel.fetch("results");
2571
+ await rel.require("staff.adjudicators");
2572
+ const where = [];
2573
+ const args = [];
2574
+ if (q.judge_clrg !== undefined) {
2575
+ where.push("judge_clrg = ?");
2576
+ args.push(q.judge_clrg);
2577
+ }
2578
+ if (q.year !== undefined) {
2579
+ where.push("year = ?");
2580
+ args.push(q.year);
2581
+ }
2582
+ const rows = rel.db("results").query(`SELECT judge_clrg, year, cat, name_printed, country, country_printed, country_code, region,
2583
+ region_printed, region_code, geohash, src
2584
+ FROM adjudicators${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`}
2585
+ ORDER BY judge_clrg, year`).all(...args);
2586
+ return { tag: rel.tag, asked: q, rows };
2587
+ }
2588
+
2249
2589
  var ENCODER2 = roleEncoder();
2250
2590
  var WHO = { table: "who", value: "entry", valueType: "INTEGER PRIMARY KEY" };
2251
2591
  var ASKED2 = ["surname_key", "nn4", "id"];
@@ -2363,6 +2703,9 @@ class StaffFinder2 {
2363
2703
  adjudicator(asked) {
2364
2704
  return this.find(asked, "adjudicator");
2365
2705
  }
2706
+ adjudicators(q = {}) {
2707
+ return registerRows(this.rel, q);
2708
+ }
2366
2709
  async find(asked, role) {
2367
2710
  const db = await this.rel.indexDb("staff");
2368
2711
  const a = asked.trim();
@@ -2507,8 +2850,8 @@ async function parquetColumns(path) {
2507
2850
  return new Set(parquetSchema(meta).children.map((c) => c.element.name));
2508
2851
  }
2509
2852
 
2510
- var str = (v) => typeof v === "string" && v !== "" ? v : null;
2511
- var num = (v) => typeof v === "number" ? v : null;
2853
+ var str2 = (v) => typeof v === "string" && v !== "" ? v : null;
2854
+ var num2 = (v) => typeof v === "number" ? v : null;
2512
2855
  var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
2513
2856
  var scope = (id) => id === null ? null : id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable";
2514
2857
  var TEAM_TABLES = [
@@ -2563,9 +2906,9 @@ var teamsIndex = {
2563
2906
  const ey = new Map;
2564
2907
  const putEy = out.prepare("INSERT OR IGNORE INTO event_year VALUES (?, ?, ?)");
2565
2908
  for (const r of t.event_years) {
2566
- const k = str(r.event_year);
2567
- const event = str(r.event);
2568
- const year = num(r.year);
2909
+ const k = str2(r.event_year);
2910
+ const event = str2(r.event);
2911
+ const year = num2(r.year);
2569
2912
  if (k === null || event === null || year === null)
2570
2913
  continue;
2571
2914
  ey.set(k, { event, year });
@@ -2574,19 +2917,19 @@ var teamsIndex = {
2574
2917
  const putR = out.prepare("INSERT OR IGNORE INTO result VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
2575
2918
  let ord = 0;
2576
2919
  for (const r of t.team_results)
2577
- putR.run(str(r.address), str(r.slug), str(r.event), num(r.year), str(r.card), str(r.comp_no), str(r.title), str(r.category), num(r.hands), str(r.letter), str(r.label), str(r.name_printed), str(r.school_printed), str(r.school_loc), str(r.school_id), num(r.place), str(r.place_printed), num(r.tie), num(r.competitors), JSON.stringify(strs(r.atts)), str(r.age), str(r.gender), JSON.stringify(strs(r.printed_status)), ord++);
2920
+ putR.run(str2(r.address), str2(r.slug), str2(r.event), num2(r.year), str2(r.card), str2(r.comp_no), str2(r.title), str2(r.category), num2(r.hands), str2(r.letter), str2(r.label), str2(r.name_printed), str2(r.school_printed), str2(r.school_loc), str2(r.school_id), num2(r.place), str2(r.place_printed), num2(r.tie), num2(r.competitors), JSON.stringify(strs(r.atts)), str2(r.age), str2(r.gender), JSON.stringify(strs(r.printed_status)), ord++);
2578
2921
  const putT = out.prepare("INSERT OR IGNORE INTO roster_team VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
2579
2922
  for (const r of t.roster_teams) {
2580
- const e = ey.get(str(r.event_year) ?? "");
2581
- putT.run(str(r.team), str(r.kind), e?.event ?? null, num(r.year) ?? e?.year ?? null, str(r.comp_no), str(r.title), str(r.team_index), num(r.seq), str(r.school_id), str(r.school_print_materialized), str(r.dance), str(r.school_print_as_of), ord++);
2923
+ const e = ey.get(str2(r.event_year) ?? "");
2924
+ putT.run(str2(r.team), str2(r.kind), e?.event ?? null, num2(r.year) ?? e?.year ?? null, str2(r.comp_no), str2(r.title), str2(r.team_index), num2(r.seq), str2(r.school_id), str2(r.school_print_materialized), str2(r.dance), str2(r.school_print_as_of), ord++);
2582
2925
  }
2583
2926
  const putM = out.prepare("INSERT INTO roster_member VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
2584
2927
  for (const r of t.roster_members)
2585
- putM.run(str(r.team), num(r.seq), typeof r.name_printed === "string" ? r.name_printed : "", str(r.name), num(r.yob), num(r.dancer_id), str(r.gender), ord++);
2928
+ putM.run(str2(r.team), num2(r.seq), typeof r.name_printed === "string" ? r.name_printed : "", str2(r.name), num2(r.yob), num2(r.dancer_id), str2(r.gender), ord++);
2586
2929
  const putJ = out.prepare("INSERT INTO result_roster VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
2587
2930
  for (const r of t.team_result_rosters) {
2588
- const e = ey.get(str(r.event_year) ?? "");
2589
- putJ.run(str(r.team), e?.event ?? null, e?.year ?? null, str(r.comp_no), str(r.school_id), str(r.result_address), str(r.join_kind), num(r.via_split) === 1 ? 1 : 0, num(r.roster_teams), num(r.result_teams), str(r.roster_index), str(r.result_letter), ord++);
2931
+ const e = ey.get(str2(r.event_year) ?? "");
2932
+ putJ.run(str2(r.team), e?.event ?? null, e?.year ?? null, str2(r.comp_no), str2(r.school_id), str2(r.result_address), str2(r.join_kind), num2(r.via_split) === 1 ? 1 : 0, num2(r.roster_teams), num2(r.result_teams), str2(r.roster_index), str2(r.result_letter), ord++);
2590
2933
  }
2591
2934
  for (const s of TEAMS_INDEXES)
2592
2935
  out.run(s);
@@ -2606,11 +2949,11 @@ var eventsIndex = {
2606
2949
  out.run(EVENTS_SCHEMA);
2607
2950
  const put = out.prepare("INSERT OR IGNORE INTO event_year VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
2608
2951
  for (const r of rows) {
2609
- const event = str(r.event);
2610
- const year = num(r.year);
2952
+ const event = str2(r.event);
2953
+ const year = num2(r.year);
2611
2954
  if (event === null || year === null)
2612
2955
  continue;
2613
- put.run(event, year, str(r.date_lo), str(r.date_hi), str(r.resolution), str(r.source), JSON.stringify(strs(r.date_printed)), str(r.place), JSON.stringify(strs(r.place_printed)));
2956
+ put.run(event, year, str2(r.date_lo), str2(r.date_hi), str2(r.resolution), str2(r.source), JSON.stringify(strs(r.date_printed)), str2(r.place), JSON.stringify(strs(r.place_printed)));
2614
2957
  }
2615
2958
  out.run("CREATE INDEX event_year_event ON event_year(event COLLATE NOCASE)");
2616
2959
  }
@@ -3238,7 +3581,7 @@ var FEATURES2 = [
3238
3581
  feature: "schools.lineage",
3239
3582
  needs: ["school-lineage.jsonl"],
3240
3583
  since: 184,
3241
- calls: "schools.links and schools.line (refused on a release without it), and the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
3584
+ calls: "schools.links, schools.line and schools.lineage (refused on a release without it), and the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
3242
3585
  },
3243
3586
  {
3244
3587
  feature: "results.stream",
@@ -3396,6 +3739,94 @@ var FEATURES2 = [
3396
3739
  since: 260,
3397
3740
  calls: "events.years (the events index)"
3398
3741
  },
3742
+ {
3743
+ feature: "upcoming.feises",
3744
+ needs: [
3745
+ "occurrence_id",
3746
+ "occurrence_ids",
3747
+ "name_printed",
3748
+ "date_lo",
3749
+ "date_hi",
3750
+ "year",
3751
+ "hosts",
3752
+ "feis_brand_id",
3753
+ "feis_instance_id",
3754
+ "series_basis",
3755
+ "brand_candidates",
3756
+ "captured_id",
3757
+ "results_state"
3758
+ ].map((c) => `results:upcoming_feises.${c}`),
3759
+ since: 285,
3760
+ calls: "upcoming.feises, the `next` of feises.series"
3761
+ },
3762
+ {
3763
+ feature: "upcoming.fields",
3764
+ needs: [
3765
+ "field_id",
3766
+ "feis_id",
3767
+ "source",
3768
+ "host_id",
3769
+ "competition_code",
3770
+ "count_only",
3771
+ "occurrence_id",
3772
+ "name_printed",
3773
+ "date_lo",
3774
+ "date_hi",
3775
+ "year",
3776
+ "feis_brand_id",
3777
+ "comp_arm",
3778
+ "comp_source",
3779
+ "comp_kind",
3780
+ "level",
3781
+ "feis_level",
3782
+ "age",
3783
+ "population",
3784
+ "comp_title",
3785
+ "comp_why",
3786
+ "state",
3787
+ "results_competitions",
3788
+ "results_feis_ids",
3789
+ "version",
3790
+ "versions",
3791
+ "captured_date",
3792
+ "sha256",
3793
+ "limits",
3794
+ "entrants",
3795
+ "stated_rows",
3796
+ "printed_total"
3797
+ ].map((c) => `results:upcoming_fields.${c}`),
3798
+ since: 291,
3799
+ calls: "upcoming.fields (285 and 290 carry the table empty)"
3800
+ },
3801
+ {
3802
+ feature: "upcoming.entrants",
3803
+ needs: [
3804
+ "feis_id",
3805
+ "captured_id",
3806
+ "row_id",
3807
+ "source",
3808
+ "host_id",
3809
+ "row_kind",
3810
+ "count_only",
3811
+ "n",
3812
+ "name_printed",
3813
+ "school_printed",
3814
+ "school_id",
3815
+ "school_rung",
3816
+ "school_candidates",
3817
+ "competitions",
3818
+ "field_ids",
3819
+ "level_counts",
3820
+ "yob",
3821
+ "asked",
3822
+ "arm",
3823
+ "candidate_ids",
3824
+ "candidates",
3825
+ "flags"
3826
+ ].map((c) => `results:upcoming_entrants.${c}`),
3827
+ since: 291,
3828
+ calls: "upcoming.entrants (285 and 290 carry the table empty)"
3829
+ },
3399
3830
  {
3400
3831
  feature: "staff",
3401
3832
  needs: [
@@ -3407,17 +3838,102 @@ var FEATURES2 = [
3407
3838
  ],
3408
3839
  since: 260,
3409
3840
  calls: "staff.teacher, staff.adjudicator (the staff index)"
3841
+ },
3842
+ { feature: "tabs", needs: TAB_NEEDS, since: 184, calls: "tabs.query, tabs.stream" },
3843
+ {
3844
+ feature: "panels",
3845
+ needs: [
3846
+ "slug",
3847
+ "panel",
3848
+ "round",
3849
+ "seat_index",
3850
+ "address",
3851
+ "judge_name_printed",
3852
+ "judge_clrg",
3853
+ "judge_id",
3854
+ "judge_unmatched"
3855
+ ].map((c) => `results:panels.${c}`),
3856
+ since: 184,
3857
+ calls: "panels.query"
3858
+ },
3859
+ {
3860
+ feature: "feis-panels",
3861
+ needs: [
3862
+ "address",
3863
+ "platform",
3864
+ "round",
3865
+ "seat_index",
3866
+ "panel_size",
3867
+ "panel_kind",
3868
+ "judge_name_printed",
3869
+ "judge_state_printed",
3870
+ "judge_clrg",
3871
+ "judge_id",
3872
+ "judge_unmatched",
3873
+ "judge_basis"
3874
+ ].map((c) => `results:feis_panels.${c}`),
3875
+ since: 260,
3876
+ calls: "feisPanels.query"
3877
+ },
3878
+ {
3879
+ feature: "staff.adjudicators",
3880
+ needs: [
3881
+ "judge_clrg",
3882
+ "year",
3883
+ "cat",
3884
+ "name_printed",
3885
+ "country",
3886
+ "country_printed",
3887
+ "country_code",
3888
+ "region",
3889
+ "region_printed",
3890
+ "region_code",
3891
+ "geohash",
3892
+ "src"
3893
+ ].map((c) => `results:adjudicators.${c}`),
3894
+ since: 230,
3895
+ calls: "staff.adjudicators"
3896
+ },
3897
+ {
3898
+ feature: "schools.registry",
3899
+ needs: ["school-registry.jsonl"],
3900
+ since: 260,
3901
+ calls: "schools.list"
3902
+ },
3903
+ {
3904
+ feature: "schools.prints",
3905
+ needs: [
3906
+ "results:results.school_printed",
3907
+ "results:results.school_id",
3908
+ "results:results.competition_line",
3909
+ "results:competitions.event",
3910
+ "results:competitions.year"
3911
+ ],
3912
+ since: 184,
3913
+ calls: "schools.prints"
3914
+ },
3915
+ {
3916
+ feature: "schools.redirects",
3917
+ needs: ["school-redirects.jsonl"],
3918
+ since: 184,
3919
+ calls: "schools.redirects"
3920
+ },
3921
+ {
3922
+ feature: "schools.associations",
3923
+ needs: ["observed-associations.jsonl"],
3924
+ since: 215,
3925
+ calls: "schools.associations"
3410
3926
  }
3411
3927
  ];
3412
3928
  async function hasNeed(probe, n) {
3413
- const t = /^table:([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
3929
+ const t = /^table:([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?$/.exec(n);
3414
3930
  if (t !== null) {
3415
3931
  if (probe.tableColumns === undefined)
3416
3932
  return false;
3417
3933
  const cols = await probe.tableColumns(t[1]);
3418
3934
  return t[2] === undefined ? cols.size > 0 : cols.has(t[2]);
3419
3935
  }
3420
- const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
3936
+ const m = /^(results|majors):([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?$/.exec(n);
3421
3937
  if (m === null)
3422
3938
  return probe.publishes(n);
3423
3939
  const cols = columnsOf2(await probe.db(m[1]), m[2]);
@@ -3434,6 +3950,186 @@ function needsRelease2(tag, c) {
3434
3950
  return new Error(`archive-client: ${tag} is too old for ${c.feature} (${c.calls}): it lacks ${c.missing.join(", ")}. Open a release of cut ${c.since} or later`);
3435
3951
  }
3436
3952
 
3953
+ var FIELD_STATES2 = ["pending", "results", "passed", "withdrawn"];
3954
+ var json = (s, empty) => typeof s === "string" && s !== "" ? JSON.parse(s) : empty;
3955
+ function fieldOf(r) {
3956
+ const countOnly = r.count_only === 1;
3957
+ return {
3958
+ field_id: r.field_id,
3959
+ feis_id: r.feis_id,
3960
+ source: r.source,
3961
+ host_id: r.host_id,
3962
+ competition_code: r.competition_code ?? null,
3963
+ count_only: countOnly,
3964
+ occurrence_id: r.occurrence_id ?? null,
3965
+ name_printed: r.name_printed,
3966
+ date_lo: r.date_lo ?? null,
3967
+ date_hi: r.date_hi ?? null,
3968
+ year: r.year,
3969
+ feis_brand_id: r.feis_brand_id ?? null,
3970
+ comp: r.comp_arm === null ? null : {
3971
+ arm: r.comp_arm,
3972
+ source: r.comp_source ?? null,
3973
+ kind: r.comp_kind ?? null,
3974
+ level: r.level ?? null,
3975
+ feis_level: r.feis_level ?? null,
3976
+ age: r.age ?? null,
3977
+ population: r.population ?? null,
3978
+ title: r.comp_title ?? null,
3979
+ why: r.comp_why ?? null
3980
+ },
3981
+ state: r.state,
3982
+ results_competitions: json(r.results_competitions, []),
3983
+ results_feis_ids: json(r.results_feis_ids, []),
3984
+ version: r.version,
3985
+ versions: json(r.versions, []),
3986
+ captured_date: r.captured_date,
3987
+ sha256: r.sha256,
3988
+ limits: json(r.limits, []),
3989
+ entrants: r.entrants,
3990
+ stated_rows: r.stated_rows ?? null,
3991
+ printed_total: json(r.printed_total, null)
3992
+ };
3993
+ }
3994
+ function entrantOf(r) {
3995
+ return {
3996
+ feis_id: r.feis_id,
3997
+ captured_id: r.captured_id,
3998
+ row_id: r.row_id,
3999
+ source: r.source,
4000
+ host_id: r.host_id,
4001
+ row_kind: r.row_kind,
4002
+ count_only: r.count_only === 1,
4003
+ n: r.n,
4004
+ name_printed: r.name_printed ?? null,
4005
+ school_printed: r.school_printed ?? null,
4006
+ school_id: r.school_id ?? null,
4007
+ school_rung: r.school_rung,
4008
+ school_candidates: json(r.school_candidates, []),
4009
+ competitions: json(r.competitions, []),
4010
+ field_ids: json(r.field_ids, []),
4011
+ level_counts: json(r.level_counts, null),
4012
+ yob: r.yob ?? null,
4013
+ asked: json(r.asked, null),
4014
+ arm: r.arm ?? null,
4015
+ candidates: json(r.candidates, []),
4016
+ flags: json(r.flags, [])
4017
+ };
4018
+ }
4019
+ function feisOf(r) {
4020
+ return {
4021
+ occurrence_id: r.occurrence_id,
4022
+ occurrence_ids: json(r.occurrence_ids, []),
4023
+ name_printed: r.name_printed,
4024
+ date_lo: r.date_lo,
4025
+ date_hi: r.date_hi ?? null,
4026
+ year: r.year,
4027
+ hosts: json(r.hosts, []),
4028
+ feis_brand_id: r.feis_brand_id ?? null,
4029
+ feis_instance_id: r.feis_instance_id ?? null,
4030
+ series_basis: r.series_basis,
4031
+ brand_candidates: json(r.brand_candidates, []),
4032
+ captured_id: r.captured_id,
4033
+ state: r.results_state
4034
+ };
4035
+ }
4036
+
4037
+ class UpcomingFinder2 {
4038
+ rel;
4039
+ constructor(rel) {
4040
+ this.rel = rel;
4041
+ }
4042
+ async db(feature) {
4043
+ await this.rel.fetch("results");
4044
+ await this.rel.require(feature);
4045
+ return this.rel.db("results");
4046
+ }
4047
+ async feises(q = {}) {
4048
+ const db = await this.db("upcoming.feises");
4049
+ const where = [];
4050
+ const args = [];
4051
+ if (q.state !== undefined) {
4052
+ where.push("results_state = ?");
4053
+ args.push(q.state);
4054
+ }
4055
+ if (q.brand !== undefined) {
4056
+ where.push("feis_brand_id = ?");
4057
+ args.push(q.brand);
4058
+ }
4059
+ if (q.from !== undefined) {
4060
+ where.push("date_lo >= ?");
4061
+ args.push(q.from);
4062
+ }
4063
+ if (q.to !== undefined) {
4064
+ where.push("date_lo <= ?");
4065
+ args.push(q.to);
4066
+ }
4067
+ const sql = `SELECT * FROM upcoming_feises${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date_lo, occurrence_id`;
4068
+ const rows = db.query(sql).all(...args);
4069
+ return { tag: this.rel.tag, feises: rows.map(feisOf) };
4070
+ }
4071
+ async fields(q = {}) {
4072
+ const db = await this.db("upcoming.fields");
4073
+ const where = [];
4074
+ const args = [];
4075
+ if (q.state !== undefined) {
4076
+ const states = typeof q.state === "string" ? [q.state] : [...q.state];
4077
+ if (states.length === 0)
4078
+ return { tag: this.rel.tag, fields: [] };
4079
+ where.push(`state IN (${states.map(() => "?").join(", ")})`);
4080
+ args.push(...states);
4081
+ }
4082
+ if (q.feis !== undefined) {
4083
+ where.push("(feis_id = ? OR feis_brand_id = ? OR occurrence_id = ?)");
4084
+ args.push(q.feis, q.feis, q.feis);
4085
+ }
4086
+ if (q.field_id !== undefined) {
4087
+ where.push("field_id = ?");
4088
+ args.push(q.field_id);
4089
+ }
4090
+ if (q.from !== undefined) {
4091
+ where.push("date_lo >= ?");
4092
+ args.push(q.from);
4093
+ }
4094
+ if (q.to !== undefined) {
4095
+ where.push("date_lo <= ?");
4096
+ args.push(q.to);
4097
+ }
4098
+ const sql = `SELECT * FROM upcoming_fields${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date_lo, feis_id, field_id`;
4099
+ const rows = db.query(sql).all(...args);
4100
+ return { tag: this.rel.tag, fields: rows.map(fieldOf) };
4101
+ }
4102
+ async entrants(q) {
4103
+ const [field] = (await this.fields({ field_id: q.field_id })).fields;
4104
+ const db = await this.db("upcoming.entrants");
4105
+ if (field === undefined)
4106
+ return {
4107
+ tag: this.rel.tag,
4108
+ field: null,
4109
+ version: null,
4110
+ entrants: [],
4111
+ why: `the release holds no field "${q.field_id}"`
4112
+ };
4113
+ const version = q.version ?? field.version;
4114
+ if (!field.versions.includes(version))
4115
+ return {
4116
+ tag: this.rel.tag,
4117
+ field,
4118
+ version,
4119
+ entrants: [],
4120
+ why: `${field.field_id} has no version ${version} (versions ${field.versions.join(", ")})`
4121
+ };
4122
+ const rows = db.query("SELECT * FROM upcoming_entrants WHERE feis_id = ? AND captured_id = ? ORDER BY row_id").all(field.feis_id, version).map(entrantOf).filter((e) => e.field_ids.includes(field.field_id));
4123
+ return {
4124
+ tag: this.rel.tag,
4125
+ field,
4126
+ version,
4127
+ entrants: rows,
4128
+ ...rows.length === 0 ? { why: `${field.field_id} lists no row in version ${version}` } : {}
4129
+ };
4130
+ }
4131
+ }
4132
+
3437
4133
  function tagDir(root, tag) {
3438
4134
  if (parseArchiveTag2(tag) === null)
3439
4135
  throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
@@ -3490,6 +4186,10 @@ class Release2 {
3490
4186
  registrations;
3491
4187
  teams;
3492
4188
  events;
4189
+ upcoming;
4190
+ tabs;
4191
+ panels;
4192
+ feisPanels;
3493
4193
  opts;
3494
4194
  handles = new Map;
3495
4195
  remoteAssets = null;
@@ -3512,6 +4212,10 @@ class Release2 {
3512
4212
  this.registrations = new RegistrationResolver2(this, this.comps, opts.dancerResolver);
3513
4213
  this.teams = new TeamFinder2(this);
3514
4214
  this.events = new EventFinder2(this);
4215
+ this.upcoming = new UpcomingFinder2(this);
4216
+ this.tabs = new TabFinder2(this);
4217
+ this.panels = new PanelFinder2(this);
4218
+ this.feisPanels = new FeisPanelFinder2(this);
3515
4219
  }
3516
4220
  receipt() {
3517
4221
  return readReceipt(this.dir) ?? { tag: this.tag, source: "unknown", files: {} };
@@ -4232,4 +4936,4 @@ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
4232
4936
  return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
4233
4937
  }
4234
4938
 
4235
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };
4939
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, NO_UPCOMING2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, TabFinder2, PanelFinder2, FeisPanelFinder2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, FIELD_STATES2, UpcomingFinder2, resolveTag2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };