@finbheara/archive-client 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Open a versioned release of an Irish dance results dataset and work with it locally: download,
4
4
  verify, decompress and cache it by tag, keep a user-local cache of releases, and resolve feises,
5
- schools, competitions, registration lists, dancers, results, majors, teachers and
6
- adjudicators against one. A command line, `idq`, is a thin wrapper over the same calls.
5
+ schools, competitions, registration lists, dancers, results, majors, teams, event dates, teachers
6
+ and adjudicators against one. A command line, `idq`, is a thin wrapper over the same calls.
7
7
 
8
8
  Bun 1.4.2 or later (the databases open through `bun:sqlite`). MIT.
9
9
 
@@ -20,7 +20,7 @@ type declarations (`.d.ts`), so a TypeScript project needs Bun's types (`"types"
20
20
  `bunx idq <verb>`.
21
21
 
22
22
  The version is 0.x while the API settles: a minor bump may break, so pin a minor
23
- (`"@finbheara/archive-client": "0.2.x"`).
23
+ (`"@finbheara/archive-client": "0.4.x"`).
24
24
 
25
25
  ## Configuration
26
26
 
@@ -85,6 +85,8 @@ c.features; // [{ feature: "dancers", state: "full" | "missing", missing: [...
85
85
  | `staff` | `staff.teacher`, `staff.adjudicator` | 260 |
86
86
  | `majors.order` | `majors.competitions` | 260 |
87
87
  | `feises`, `comps` | `feises.find`, `feises.series`, `comps.*`, `registrations.resolve` | 265 |
88
+ | `teams` | `teams.results`, `teams.rosters`, `teams.resultRosters` | 260 |
89
+ | `events` | `events.years` | 260 |
88
90
 
89
91
  A spec is `pinned` (the default: the user-local pin, else the latest), `latest`, a cut number
90
92
  (`42`, or `42.1` for a fraction) or a tag (`archive-YYYY.MM.DD-N`).
@@ -138,8 +140,9 @@ outside the cache root.
138
140
 
139
141
  The release databases carry a primary key per table and nothing more, so lookups build side
140
142
  indexes, one SQLite file per index per tag, each keyed on its builder's version and the sha256 of
141
- every input it read. A file whose key is stale is rebuilt, never served. Four ship here, and a
142
- caller may hand its own (`builders` option, an `IndexBuilder`):
143
+ every input it read. A file whose key is stale is rebuilt, never served. These ship here, and a
144
+ caller may hand its own (`builders` option, an `IndexBuilder`, with an optional async `load` step
145
+ whose answer its `build` receives):
143
146
 
144
147
  - `schools`: every published school print with the ids its rows carry, filed into a
145
148
  `@finbheara/domain` `schoolEncoder` noun index, with the registry and the retired ids
@@ -151,6 +154,9 @@ caller may hand its own (`builders` option, an `IndexBuilder`):
151
154
  - `staff` (keyed the same way): teachers and adjudicators by name (a `roleEncoder` noun index) and TD number: the
152
155
  schools' staff lists and TD links, the adjudicator register, and the seats each sat
153
156
  - `comps`: every competition by its feis and the feis's own code, with its series and year
157
+ - `teams`: the team results, the roster teams with their members, and how they join, by
158
+ event-year
159
+ - `events`: every event-year's dates and place
154
160
 
155
161
  ## Feises
156
162
 
@@ -189,7 +195,8 @@ print exactly as the release published it, then the print read by `@finbheara/do
189
195
  reader at its filing key (`sn3`) and at its real words. `ambiguous` and `none` are answers: a
190
196
  best guess is never promoted to `unique`. `region` drops only a candidate the registry places in
191
197
  other regions, and keeps one it places nowhere. A print the reader refuses (a placeholder)
192
- answers `none` with the reason.
198
+ answers `none` with the reason. An id (or a retired id followed) answers with every print its
199
+ rows were published under and their rows, so it agrees with each print lookup of that school.
193
200
 
194
201
  Every answer carries the release tag it came from.
195
202
 
@@ -269,6 +276,32 @@ A row carries the prints and the ids that stand for them: the address, the compe
269
276
  and age, the place, the name and school as printed with the dancer and school ids. The majors are
270
277
  read from `majors.sqlite`, fetched on first use.
271
278
 
279
+ ## Teams and event-years
280
+
281
+ ```ts
282
+ const r = await rel.teams.results({ event: "<event>", year: 2024 });
283
+ r.results; // [{ address, competition, event, year, comp_no, title, category, hands, letter, label,
284
+ // school_printed, school_id, place, place_printed, tie, competitors, atts, ... }]
285
+ const t = await rel.teams.rosters({ event: "<event>", year: 2024 });
286
+ t.teams; // [{ team, kind, event, year, comp_no, title, team_index, seq, school_id, school_printed,
287
+ // members: [{ seq, name_printed, name, yob, dancer_id, dancer_scope }] }]
288
+ const j = await rel.teams.resultRosters({ event: "<event>", year: 2024 });
289
+ j.joins; // [{ team, result_address, join_kind, via_split, roster_index, result_letter, ... }]
290
+ const e = await rel.events.years({ event: "<event>" });
291
+ e.years; // [{ event, year, date_lo, date_hi, resolution, source, date_printed, place, place_printed }]
292
+ ```
293
+
294
+ A query names an event (case-blind), a year, both, or neither (every row). Everything comes from
295
+ ONE release: a roster member's `dancer_id` is the same release's id as the results', so a caller
296
+ never joins team records of one release to dancer ids of another. A roster is a sheet of a school's
297
+ teams (`kind: "rosterTeam"`) or a result card's own line-up (`lineup`). `join_kind` says how a
298
+ roster team met the results (`exact`, `card`, `branch`) or why it did not (`no-result`,
299
+ `no-school`, `no-roster`, `multi-team-unjoined`), and `result_address` is null where none joined.
300
+ An event-year's dates are a window (`date_lo`, `date_hi`) with how fine it is (`resolution`:
301
+ `day`, `month`, `calendar`) and what it is read from (`source`), and `place` is a place-tree node
302
+ where the release knows one. A field the source left empty is null. The rows are read once per
303
+ release into the `teams` and `events` indexes.
304
+
272
305
  ## Teachers and adjudicators
273
306
 
274
307
  ```ts
@@ -393,6 +426,8 @@ and the competitions still resolve.
393
426
  [--population girls|boys] [--rows]
394
427
  idq results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <c>] [--school <s>] [--year <y>] [--limit <n>]
395
428
  idq majors [--release <spec>] [--event <e>] [--year <y>] [--comp <c>] [--dancer <id>] [--rows]
429
+ idq teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]
430
+ idq events [--release <spec>] [--event <e>] [--year <y>]
396
431
  idq teacher [--release <spec>] <name|TD number>
397
432
  idq adjudicator [--release <spec>] <name|TD number>
398
433
  idq comp [--release <spec>] <feis> <code> [--year y] [--title t]
@@ -1459,7 +1459,7 @@ var encoderOf = (r) => schoolEncoder({ reader: r.reader });
1459
1459
  var TABLE = "school_name";
1460
1460
  var schoolsIndex = {
1461
1461
  name: "schools",
1462
- version: "1",
1462
+ version: "2",
1463
1463
  inputs: ["results", "school-registry.jsonl", "school-redirects.jsonl"],
1464
1464
  says: "every published school print and its ids, read at the school reader's slots",
1465
1465
  keyParts: (rel) => [`reader=${rel.schoolReader().id}`],
@@ -1475,6 +1475,7 @@ var schoolsIndex = {
1475
1475
  const putPair = out.prepare("INSERT INTO print_id VALUES (?, ?, ?)");
1476
1476
  for (const r of pairs)
1477
1477
  putPair.run(r.p, r.s, r.n);
1478
+ out.run("CREATE INDEX print_id_school ON print_id(school_id)");
1478
1479
  const names = createIndex3(out, encoder, {
1479
1480
  table: TABLE
1480
1481
  });
@@ -1534,9 +1535,13 @@ class SchoolResolver2 {
1534
1535
  ...extra
1535
1536
  };
1536
1537
  };
1538
+ const byId = (id, by) => {
1539
+ const rs = db.query("SELECT print, rows FROM print_id WHERE school_id = ? ORDER BY print").all(id);
1540
+ return candidate(id, by, rs.reduce((n, r) => n + r.rows, 0), rs.map((r) => r.print));
1541
+ };
1537
1542
  const a = asked.trim();
1538
1543
  if (reg.get(a) !== null)
1539
- return answer([candidate(a, "id", 0, [])]);
1544
+ return answer([byId(a, "id")]);
1540
1545
  let live = a;
1541
1546
  const seen = new Set;
1542
1547
  for (;; ) {
@@ -1547,7 +1552,7 @@ class SchoolResolver2 {
1547
1552
  live = r.new_id;
1548
1553
  }
1549
1554
  if (live !== a)
1550
- return answer([candidate(live, "redirect", 0, [])], { redirected: { from: a, to: live } });
1555
+ return answer([byId(live, "redirect")], { redirected: { from: a, to: live } });
1551
1556
  const byPrints = (prints, by) => {
1552
1557
  const ids = new Map;
1553
1558
  const q2 = db.query("SELECT school_id, rows FROM print_id WHERE print = ?");
@@ -1581,8 +1586,240 @@ class SchoolResolver2 {
1581
1586
  }
1582
1587
  }
1583
1588
 
1589
+ import { parquetMetadata, parquetReadObjects, parquetSchema } from "hyparquet";
1590
+ var COMPRESSORS = {
1591
+ ZSTD: (input) => new Uint8Array(Bun.zstdDecompressSync(input))
1592
+ };
1593
+ var tableAsset = (table) => `${table}.parquet`;
1594
+ function plain(v) {
1595
+ if (typeof v === "bigint")
1596
+ return v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v;
1597
+ if (Array.isArray(v))
1598
+ return v.map(plain);
1599
+ if (v !== null && typeof v === "object" && !(v instanceof Date)) {
1600
+ const o = {};
1601
+ for (const [k, x] of Object.entries(v))
1602
+ o[k] = plain(x);
1603
+ return o;
1604
+ }
1605
+ return v;
1606
+ }
1607
+ async function readParquet(path, columns) {
1608
+ const file = await Bun.file(path).arrayBuffer();
1609
+ const rows = await parquetReadObjects({
1610
+ file,
1611
+ compressors: COMPRESSORS,
1612
+ ...columns !== undefined ? { columns: [...columns] } : {}
1613
+ });
1614
+ return rows.map((r) => plain(r));
1615
+ }
1616
+ async function parquetColumns(path) {
1617
+ const meta = parquetMetadata(await Bun.file(path).arrayBuffer());
1618
+ return new Set(parquetSchema(meta).children.map((c) => c.element.name));
1619
+ }
1620
+
1621
+ var str = (v) => typeof v === "string" && v !== "" ? v : null;
1622
+ var num = (v) => typeof v === "number" ? v : null;
1623
+ var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
1624
+ var scope = (id) => id === null ? null : id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable";
1625
+ var TEAM_TABLES = [
1626
+ "team_results",
1627
+ "roster_teams",
1628
+ "roster_members",
1629
+ "team_result_rosters",
1630
+ "event_years"
1631
+ ];
1632
+ var TEAMS_SCHEMA = `
1633
+ CREATE TABLE event_year(event_year TEXT PRIMARY KEY, event TEXT NOT NULL, year INTEGER NOT NULL);
1634
+ CREATE TABLE result(address TEXT PRIMARY KEY, competition TEXT, event TEXT, year INTEGER,
1635
+ card TEXT, comp_no TEXT, title TEXT, category TEXT, hands INTEGER, letter TEXT, label TEXT,
1636
+ name_printed TEXT, school_printed TEXT, school_loc TEXT, school_id TEXT, place INTEGER,
1637
+ place_printed TEXT, tie INTEGER, competitors INTEGER, atts TEXT, ord INTEGER);
1638
+ CREATE TABLE roster_team(team TEXT PRIMARY KEY, kind TEXT, event TEXT, year INTEGER,
1639
+ comp_no TEXT, title TEXT, team_index TEXT, seq INTEGER, school_id TEXT, school_printed TEXT,
1640
+ ord INTEGER);
1641
+ CREATE TABLE roster_member(team TEXT, seq INTEGER, name_printed TEXT, name TEXT, yob INTEGER,
1642
+ dancer_id INTEGER, ord INTEGER);
1643
+ CREATE TABLE result_roster(team TEXT, event TEXT, year INTEGER, comp_no TEXT, school_id TEXT,
1644
+ result_address TEXT, join_kind TEXT, via_split INTEGER, roster_teams INTEGER,
1645
+ result_teams INTEGER, roster_index TEXT, result_letter TEXT, ord INTEGER);
1646
+ `;
1647
+ var TEAMS_INDEXES = [
1648
+ "CREATE INDEX result_ey ON result(event COLLATE NOCASE, year)",
1649
+ "CREATE INDEX result_year ON result(year)",
1650
+ "CREATE INDEX roster_team_ey ON roster_team(event COLLATE NOCASE, year)",
1651
+ "CREATE INDEX roster_team_year ON roster_team(year)",
1652
+ "CREATE INDEX roster_member_team ON roster_member(team)",
1653
+ "CREATE INDEX result_roster_ey ON result_roster(event COLLATE NOCASE, year)",
1654
+ "CREATE INDEX result_roster_year ON result_roster(year)"
1655
+ ];
1656
+ async function readTables(rel, tables) {
1657
+ const out = {};
1658
+ for (const t of tables) {
1659
+ await rel.fetch(tableAsset(t));
1660
+ out[t] = await readParquet(rel.path(tableAsset(t)));
1661
+ }
1662
+ return out;
1663
+ }
1664
+ var teamsIndex = {
1665
+ name: "teams",
1666
+ version: "1",
1667
+ inputs: TEAM_TABLES.map(tableAsset),
1668
+ says: "the team results, the roster teams with their members, and how they join, by event-year",
1669
+ load: (rel) => readTables(rel, TEAM_TABLES),
1670
+ build(_rel, out, loaded) {
1671
+ const t = loaded;
1672
+ out.run(TEAMS_SCHEMA);
1673
+ const ey = new Map;
1674
+ const putEy = out.prepare("INSERT OR IGNORE INTO event_year VALUES (?, ?, ?)");
1675
+ for (const r of t.event_years) {
1676
+ const k = str(r.event_year);
1677
+ const event = str(r.event);
1678
+ const year = num(r.year);
1679
+ if (k === null || event === null || year === null)
1680
+ continue;
1681
+ ey.set(k, { event, year });
1682
+ putEy.run(k, event, year);
1683
+ }
1684
+ const putR = out.prepare("INSERT OR IGNORE INTO result VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1685
+ let ord = 0;
1686
+ for (const r of t.team_results)
1687
+ 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)), ord++);
1688
+ const putT = out.prepare("INSERT OR IGNORE INTO roster_team VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1689
+ for (const r of t.roster_teams) {
1690
+ const e = ey.get(str(r.event_year) ?? "");
1691
+ 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), ord++);
1692
+ }
1693
+ const putM = out.prepare("INSERT INTO roster_member VALUES (?, ?, ?, ?, ?, ?, ?)");
1694
+ for (const r of t.roster_members)
1695
+ 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), ord++);
1696
+ const putJ = out.prepare("INSERT INTO result_roster VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1697
+ for (const r of t.team_result_rosters) {
1698
+ const e = ey.get(str(r.event_year) ?? "");
1699
+ 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++);
1700
+ }
1701
+ for (const s of TEAMS_INDEXES)
1702
+ out.run(s);
1703
+ }
1704
+ };
1705
+ var EVENTS_SCHEMA = `CREATE TABLE event_year(event TEXT NOT NULL, year INTEGER NOT NULL,
1706
+ date_lo TEXT, date_hi TEXT, resolution TEXT, source TEXT, date_printed TEXT, place TEXT,
1707
+ place_printed TEXT, PRIMARY KEY(event, year))`;
1708
+ var eventsIndex = {
1709
+ name: "events",
1710
+ version: "1",
1711
+ inputs: [tableAsset("event_years")],
1712
+ says: "every event-year: its dates (and how fine they are) and its place",
1713
+ load: (rel) => readTables(rel, ["event_years"]),
1714
+ build(_rel, out, loaded) {
1715
+ const rows = loaded.event_years ?? [];
1716
+ out.run(EVENTS_SCHEMA);
1717
+ const put = out.prepare("INSERT OR IGNORE INTO event_year VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
1718
+ for (const r of rows) {
1719
+ const event = str(r.event);
1720
+ const year = num(r.year);
1721
+ if (event === null || year === null)
1722
+ continue;
1723
+ 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)));
1724
+ }
1725
+ out.run("CREATE INDEX event_year_event ON event_year(event COLLATE NOCASE)");
1726
+ }
1727
+ };
1728
+ function whereOf(q, prefix = "") {
1729
+ const where = [];
1730
+ const args = [];
1731
+ if (q.event !== undefined) {
1732
+ where.push(`${prefix}event = ? COLLATE NOCASE`);
1733
+ args.push(q.event);
1734
+ }
1735
+ if (q.year !== undefined) {
1736
+ where.push(`${prefix}year = ?`);
1737
+ args.push(q.year);
1738
+ }
1739
+ return { sql: where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`, args };
1740
+ }
1741
+
1742
+ class TeamFinder2 {
1743
+ rel;
1744
+ constructor(rel) {
1745
+ this.rel = rel;
1746
+ }
1747
+ db() {
1748
+ return this.rel.indexDb("teams");
1749
+ }
1750
+ async results(q = {}) {
1751
+ const db = await this.db();
1752
+ const w = whereOf(q);
1753
+ const rows = db.query(`SELECT address, competition, event, year, card, comp_no, title, category, hands, letter,
1754
+ label, name_printed, school_printed, school_loc, school_id, place, place_printed, tie,
1755
+ competitors, atts
1756
+ FROM result${w.sql} ORDER BY year, event, competition, place IS NULL, place, ord`).all(...w.args);
1757
+ return {
1758
+ tag: this.rel.tag,
1759
+ asked: q,
1760
+ results: rows.map((r) => ({ ...r, atts: JSON.parse(r.atts) }))
1761
+ };
1762
+ }
1763
+ async rosters(q = {}) {
1764
+ const db = await this.db();
1765
+ const w = whereOf(q, "t.");
1766
+ const teams = db.query(`SELECT team, kind, event, year, comp_no, title, team_index, seq, school_id, school_printed
1767
+ FROM roster_team t${w.sql} ORDER BY year, event, ord`).all(...w.args);
1768
+ const members = new Map;
1769
+ for (const m of db.query(`SELECT m.team AS team, m.seq AS seq, m.name_printed AS name_printed, m.name AS name,
1770
+ m.yob AS yob, m.dancer_id AS dancer_id
1771
+ FROM roster_member m JOIN roster_team t ON t.team = m.team${w.sql}
1772
+ ORDER BY m.team, m.seq, m.ord`).all(...w.args)) {
1773
+ const { team, ...rest } = m;
1774
+ let list = members.get(team);
1775
+ if (list === undefined)
1776
+ members.set(team, list = []);
1777
+ list.push({ ...rest, dancer_scope: scope(rest.dancer_id) });
1778
+ }
1779
+ return {
1780
+ tag: this.rel.tag,
1781
+ asked: q,
1782
+ teams: teams.map((t) => ({ ...t, members: members.get(t.team) ?? [] }))
1783
+ };
1784
+ }
1785
+ async resultRosters(q = {}) {
1786
+ const db = await this.db();
1787
+ const w = whereOf(q);
1788
+ const rows = db.query(`SELECT team, event, year, comp_no, school_id, result_address, join_kind, via_split,
1789
+ roster_teams, result_teams, roster_index, result_letter
1790
+ FROM result_roster${w.sql} ORDER BY year, event, ord`).all(...w.args);
1791
+ return {
1792
+ tag: this.rel.tag,
1793
+ asked: q,
1794
+ joins: rows.map((r) => ({ ...r, via_split: r.via_split === 1 }))
1795
+ };
1796
+ }
1797
+ }
1798
+
1799
+ class EventFinder2 {
1800
+ rel;
1801
+ constructor(rel) {
1802
+ this.rel = rel;
1803
+ }
1804
+ async years(q = {}) {
1805
+ const db = await this.rel.indexDb("events");
1806
+ const w = whereOf(q);
1807
+ const rows = db.query(`SELECT event, year, date_lo, date_hi, resolution, source, date_printed, place, place_printed
1808
+ FROM event_year${w.sql} ORDER BY year, event`).all(...w.args);
1809
+ return {
1810
+ tag: this.rel.tag,
1811
+ asked: q,
1812
+ years: rows.map((r) => ({
1813
+ ...r,
1814
+ date_printed: JSON.parse(r.date_printed),
1815
+ place_printed: JSON.parse(r.place_printed)
1816
+ }))
1817
+ };
1818
+ }
1819
+ }
1820
+
1584
1821
  function defaultBuilders2() {
1585
- return [schoolsIndex, feisesIndex, compsIndex, dancersIndex, staffIndex];
1822
+ return [schoolsIndex, feisesIndex, compsIndex, dancersIndex, staffIndex, teamsIndex, eventsIndex];
1586
1823
  }
1587
1824
  var indexDir = (rel) => join(rel.dir, "index");
1588
1825
  var indexPath2 = (rel, name) => join(indexDir(rel), `${name}.sqlite`);
@@ -1618,6 +1855,8 @@ function buildIndex2(rel, b, o) {
1618
1855
  const key = indexKey2(rel, b);
1619
1856
  if (!o.force && keyOf(out) === key)
1620
1857
  return out;
1858
+ if (b.load !== undefined && o.loaded === undefined)
1859
+ throw new Error(`archive-client: index "${b.name}" reads its inputs first: build it with ensureIndex (or rel.index)`);
1621
1860
  mkdirSync(indexDir(rel), { recursive: true });
1622
1861
  const tmp = `${out}.building.${process.pid}`;
1623
1862
  rmSync(tmp, { force: true });
@@ -1627,7 +1866,7 @@ function buildIndex2(rel, b, o) {
1627
1866
  db.run("PRAGMA journal_mode = OFF");
1628
1867
  db.run("PRAGMA synchronous = OFF");
1629
1868
  db.run("CREATE TABLE meta(k TEXT PRIMARY KEY, v TEXT NOT NULL)");
1630
- db.transaction(() => b.build(rel, db))();
1869
+ db.transaction(() => b.build(rel, db, o.loaded))();
1631
1870
  const put = db.prepare("INSERT INTO meta(k, v) VALUES (?, ?)");
1632
1871
  put.run("key", key);
1633
1872
  put.run("builder", b.name);
@@ -1644,6 +1883,12 @@ function buildIndex2(rel, b, o) {
1644
1883
  o.log?.(`index ${rel.tag} ${b.name}: built in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
1645
1884
  return out;
1646
1885
  }
1886
+ async function ensureIndex2(rel, b, o) {
1887
+ if (!o.force && isCurrent2(rel, b))
1888
+ return indexPath2(rel, b.name);
1889
+ const loaded = b.load === undefined ? undefined : await b.load(rel);
1890
+ return buildIndex2(rel, b, { ...o, loaded });
1891
+ }
1647
1892
  function listIndexes2(rel) {
1648
1893
  const dir = indexDir(rel);
1649
1894
  if (!existsSync(dir))
@@ -2053,6 +2298,81 @@ var FEATURES2 = [
2053
2298
  since: 265,
2054
2299
  calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
2055
2300
  },
2301
+ {
2302
+ feature: "teams",
2303
+ needs: [
2304
+ ...[
2305
+ "slug",
2306
+ "address",
2307
+ "event",
2308
+ "year",
2309
+ "card",
2310
+ "comp_no",
2311
+ "title",
2312
+ "category",
2313
+ "hands",
2314
+ "letter",
2315
+ "label",
2316
+ "name_printed",
2317
+ "school_printed",
2318
+ "school_loc",
2319
+ "school_id",
2320
+ "place",
2321
+ "place_printed",
2322
+ "tie",
2323
+ "competitors",
2324
+ "atts"
2325
+ ].map((c) => `table:team_results.${c}`),
2326
+ ...[
2327
+ "team",
2328
+ "kind",
2329
+ "event_year",
2330
+ "year",
2331
+ "comp_no",
2332
+ "title",
2333
+ "team_index",
2334
+ "seq",
2335
+ "school_id",
2336
+ "school_print_materialized"
2337
+ ].map((c) => `table:roster_teams.${c}`),
2338
+ ...["team", "seq", "name_printed", "name", "yob", "dancer_id"].map((c) => `table:roster_members.${c}`),
2339
+ ...[
2340
+ "event_year",
2341
+ "comp_no",
2342
+ "team",
2343
+ "result_address",
2344
+ "school_id",
2345
+ "join_kind",
2346
+ "via_split",
2347
+ "roster_teams",
2348
+ "result_teams",
2349
+ "roster_index",
2350
+ "result_letter"
2351
+ ].map((c) => `table:team_result_rosters.${c}`),
2352
+ "table:event_years.event_year",
2353
+ "table:event_years.event",
2354
+ "table:event_years.year"
2355
+ ],
2356
+ since: 260,
2357
+ calls: "teams.results, teams.rosters, teams.resultRosters (the teams index)"
2358
+ },
2359
+ {
2360
+ feature: "events",
2361
+ needs: [
2362
+ "event_year",
2363
+ "event",
2364
+ "year",
2365
+ "date_lo",
2366
+ "date_hi",
2367
+ "resolution",
2368
+ "source",
2369
+ "date_printed",
2370
+ "place",
2371
+ "place_printed"
2372
+ ].map((c) => `table:event_years.${c}`),
2373
+ since: 260,
2374
+ calls: "events.years (the events index)"
2375
+ },
2056
2376
  {
2057
2377
  feature: "staff",
2058
2378
  needs: [
@@ -2067,6 +2387,13 @@ var FEATURES2 = [
2067
2387
  }
2068
2388
  ];
2069
2389
  async function hasNeed(probe, n) {
2390
+ const t = /^table:([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
2391
+ if (t !== null) {
2392
+ if (probe.tableColumns === undefined)
2393
+ return false;
2394
+ const cols = await probe.tableColumns(t[1]);
2395
+ return t[2] === undefined ? cols.size > 0 : cols.has(t[2]);
2396
+ }
2070
2397
  const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
2071
2398
  if (m === null)
2072
2399
  return probe.publishes(n);
@@ -2138,6 +2465,8 @@ class Release2 {
2138
2465
  staff;
2139
2466
  comps;
2140
2467
  registrations;
2468
+ teams;
2469
+ events;
2141
2470
  opts;
2142
2471
  handles = new Map;
2143
2472
  remoteAssets = null;
@@ -2158,6 +2487,8 @@ class Release2 {
2158
2487
  this.staff = new StaffFinder2(this);
2159
2488
  this.comps = new CompResolver2(this);
2160
2489
  this.registrations = new RegistrationResolver2(this, this.comps, opts.dancerResolver);
2490
+ this.teams = new TeamFinder2(this);
2491
+ this.events = new EventFinder2(this);
2161
2492
  }
2162
2493
  receipt() {
2163
2494
  return readReceipt(this.dir) ?? { tag: this.tag, source: "unknown", files: {} };
@@ -2202,7 +2533,14 @@ class Release2 {
2202
2533
  await this.fetch(name);
2203
2534
  return this.db(name);
2204
2535
  },
2205
- publishes: (asset) => this.publishes(asset)
2536
+ publishes: (asset) => this.publishes(asset),
2537
+ tableColumns: async (table) => {
2538
+ const asset = tableAsset(table);
2539
+ if (!await this.publishes(asset))
2540
+ return new Set;
2541
+ await this.fetch(asset);
2542
+ return parquetColumns(this.path(asset));
2543
+ }
2206
2544
  };
2207
2545
  checked = new Map;
2208
2546
  async capability(feature) {
@@ -2299,7 +2637,7 @@ class Release2 {
2299
2637
  if (FEATURES2.some((f) => f.feature === b.name))
2300
2638
  await this.require(b.name);
2301
2639
  await this.fetch(...b.inputs);
2302
- return buildIndex2(this, b, { force: o.force ?? false, log: this.opts.log });
2640
+ return ensureIndex2(this, b, { force: o.force ?? false, log: this.opts.log });
2303
2641
  }
2304
2642
  async indexDb(name) {
2305
2643
  const key = `index:${name}`;
@@ -2518,7 +2856,7 @@ class ReleaseManager2 {
2518
2856
  await rel.fetch(...b.inputs);
2519
2857
  built.push({
2520
2858
  name,
2521
- path: buildIndex2(rel, b, { force: o.force ?? false, log: this.opts.log })
2859
+ path: await ensureIndex2(rel, b, { force: o.force ?? false, log: this.opts.log })
2522
2860
  });
2523
2861
  }
2524
2862
  rel.close();
@@ -2591,4 +2929,4 @@ function releases2(opts = {}) {
2591
2929
  return new ReleaseManager2(opts);
2592
2930
  }
2593
2931
 
2594
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, NO_CALENDAR2, FeisFinder2, StaffFinder2, SchoolResolver2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
2932
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, NO_CALENDAR2, FeisFinder2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  nameOrderOf,
5
5
  openRelease2,
6
6
  releases2
7
- } from "./chunks/index-kp811fb0.js";
7
+ } from "./chunks/index-v8e0qe9f.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
@@ -50,6 +50,12 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
50
50
  majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]
51
51
  the majors: every event and its years; an event's
52
52
  competitions; a competition's, an event's or a dancer's rows
53
+ teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]
54
+ the team result rows of an event-year; with --rosters its
55
+ roster teams and their dancers; with --joins how each roster
56
+ team meets the results
57
+ events [--release <spec>] [--event <e>] [--year <y>]
58
+ each event-year's dates and place
53
59
  teacher [--release <spec>] <name|TD number>
54
60
  the staff lists' people: where they taught, the register,
55
61
  where they sat
@@ -313,6 +319,14 @@ async function resolveVerb(rel, what, args, p, say, stdin) {
313
319
  throw new Error("resolve comp|dancer|registrations");
314
320
  }
315
321
  }
322
+ function eventYearOf(p) {
323
+ const event = p.flags.get("--event");
324
+ const year = p.flags.get("--year");
325
+ return {
326
+ ...event !== undefined ? { event } : {},
327
+ ...year !== undefined ? { year: Number(year) } : {}
328
+ };
329
+ }
316
330
  async function main(argv, io = {}) {
317
331
  const say = io.say ?? ((s) => console.log(s));
318
332
  const log = io.log ?? ((s) => console.error(s));
@@ -630,6 +644,56 @@ async function main(argv, io = {}) {
630
644
  rel.close();
631
645
  }
632
646
  }
647
+ case "teams": {
648
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
649
+ try {
650
+ const q = eventYearOf(p);
651
+ if (p.bools.has("--rosters")) {
652
+ const a = await rel.teams.rosters(q);
653
+ out(a, () => {
654
+ for (const t of a.teams) {
655
+ say(` ${t.year} ${t.event ?? "?"} ${t.comp_no ?? "-"} ${t.team_index ?? "-"} ${t.school_id ?? "(no id)"} "${t.school_printed ?? ""}" ${t.title ?? ""} [${t.kind}]`);
656
+ for (const m of t.members)
657
+ say(` ${m.seq} ${m.dancer_id ?? "-"} ${m.name_printed}${m.yob === null ? "" : ` (${m.yob})`}`);
658
+ }
659
+ say(`${a.teams.length} roster team(s) in ${a.tag}`);
660
+ });
661
+ return a.teams.length === 0 ? 1 : 0;
662
+ }
663
+ if (p.bools.has("--joins")) {
664
+ const a = await rel.teams.resultRosters(q);
665
+ out(a, () => {
666
+ for (const j of a.joins)
667
+ say(` ${j.year ?? "?"} ${j.event ?? "?"} ${j.comp_no ?? "-"} ${j.team} ${j.join_kind} ${j.result_address ?? "-"} roster ${j.roster_index ?? "-"} result ${j.result_letter ?? "-"}`);
668
+ say(`${a.joins.length} join(s) in ${a.tag}`);
669
+ });
670
+ return a.joins.length === 0 ? 1 : 0;
671
+ }
672
+ const a = await rel.teams.results(q);
673
+ out(a, () => {
674
+ for (const r of a.results)
675
+ say(` ${r.address} ${r.place_printed ?? "-"} ${r.label ?? "-"} ${r.school_id ?? "(no id)"} "${r.school_printed ?? ""}" ${r.title ?? ""}${r.atts.length > 0 ? ` [${r.atts.join(",")}]` : ""}`);
676
+ say(`${a.results.length} team result(s) in ${a.tag}`);
677
+ });
678
+ return a.results.length === 0 ? 1 : 0;
679
+ } finally {
680
+ rel.close();
681
+ }
682
+ }
683
+ case "events": {
684
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
685
+ try {
686
+ const a = await rel.events.years(eventYearOf(p));
687
+ out(a, () => {
688
+ for (const y of a.years)
689
+ say(` ${y.event} ${y.year} ${y.date_lo ?? "?"}..${y.date_hi ?? "?"} (${y.resolution ?? "-"}, ${y.source ?? "-"}) ${y.place ?? "-"}${y.place_printed.length > 0 ? ` "${y.place_printed.join("; ")}"` : ""}`);
690
+ say(`${a.years.length} event-year(s) in ${a.tag}`);
691
+ });
692
+ return a.years.length === 0 ? 1 : 0;
693
+ } finally {
694
+ rel.close();
695
+ }
696
+ }
633
697
  case "teacher":
634
698
  case "adjudicator": {
635
699
  if (rest.length === 0) {
package/dist/index.js CHANGED
@@ -17,11 +17,14 @@ import {
17
17
  FeisFinder2,
18
18
  StaffFinder2,
19
19
  SchoolResolver2,
20
+ TeamFinder2,
21
+ EventFinder2,
20
22
  defaultBuilders2,
21
23
  indexPath2,
22
24
  indexKey2,
23
25
  isCurrent2,
24
26
  buildIndex2,
27
+ ensureIndex2,
25
28
  listIndexes2,
26
29
  readPin2,
27
30
  youngest2,
@@ -41,7 +44,7 @@ import {
41
44
  openCached2,
42
45
  ReleaseManager2,
43
46
  releases2
44
- } from "./chunks/index-kp811fb0.js";
47
+ } from "./chunks/index-v8e0qe9f.js";
45
48
  import {
46
49
  cacheRoot2,
47
50
  repoOf2
@@ -60,6 +63,7 @@ export {
60
63
  CompResolver2 as CompResolver,
61
64
  DATABASES2 as DATABASES,
62
65
  DancerResolver2 as DancerResolver,
66
+ EventFinder2 as EventFinder,
63
67
  FEATURES2 as FEATURES,
64
68
  FEIS_LOCAL_ID_FLOOR2 as FEIS_LOCAL_ID_FLOOR,
65
69
  FeisFinder2 as FeisFinder,
@@ -72,6 +76,7 @@ export {
72
76
  ResultFinder2 as ResultFinder,
73
77
  SchoolResolver2 as SchoolResolver,
74
78
  StaffFinder2 as StaffFinder,
79
+ TeamFinder2 as TeamFinder,
75
80
  buildIndex2 as buildIndex,
76
81
  cacheRoot2 as cacheRoot,
77
82
  canonicalOf2 as canonicalOf,
@@ -81,6 +86,7 @@ export {
81
86
  cutOf2 as cutOf,
82
87
  dancerResolverOf2 as dancerResolverOf,
83
88
  defaultBuilders2 as defaultBuilders,
89
+ ensureIndex2 as ensureIndex,
84
90
  fallbackRemote2 as fallbackRemote,
85
91
  ghRemote2 as ghRemote,
86
92
  httpsRemote2 as httpsRemote,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import type { OpenOptions } from "./release.js";
3
- export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]\n\n config repo <owner/name> set the user-local repository the releases come from\n list [--remote] the cached releases (size, files, indexes, the pin), and\n with --remote the published tags\n fetch [--release <spec>] [--assets results,majors,<file>...]\n download, verify and cache a release (default: results)\n pin <spec> the user-local pin: what a bare open (spec \"pinned\") opens\n unpin remove the pin (a bare open is then \"latest\")\n current the pin, and whether it is cached\n add <dir> --tag <tag> add a release built locally (its MANIFEST.json and files)\n to the cache under its tag\n index [--release <spec>] [<name>...] [--force]\n build the per-tag indexes (default: every one)\n verify [--release <spec>] re-hash every cached file against its receipt\n capabilities [--release <spec>] what the release carries of what the client reads, from\n its own schema: each feature full or missing (its calls\n refuse, naming the first cut that publishes it)\n prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)\n and the pinned one\n rm <tag> [--dry-run] remove one cached release (not the pinned one)\n drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)\n feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]\n the feises a query names\n series [--release <spec>] <name|host code|feis id>\n the series a name, a host code (fweb:ABC) or any feis id\n names: its years, hosts, codes and next instance\n school [--release <spec>] <print|school_id> [--region <code>]\n a school print or id to the published schools\n dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>\n [--order surname-first|given-first] [--school <print|id>]\n [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]\n [--population girls|boys] [--rows]\n the dancers a name reaches, each with its evidence\n (name level, age, school, population) and the arm; the results of\n the one dancer where one answers (or --rows for each)\n results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]\n [--school <print|id>] [--year <y>] [--limit <n>]\n the published result rows, prints and ids\n majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]\n the majors: every event and its years; an event's\n competitions; a competition's, an event's or a dancer's rows\n teacher [--release <spec>] <name|TD number>\n the staff lists' people: where they taught, the register,\n where they sat\n adjudicator [--release <spec>] <name|TD number>\n the register's and the panels' people: the same answer\n comp [--release <spec>] <feis> <code> [--year <y>] [--title <t>]\n a feis's own comp code to the comp it names, through that\n feis's syllabus (or the prior year's)\n syllabus [--release <spec>] <feis> [year]\n one feis-year's comps, in printed order\n syllabus [--release <spec>] --for <series> [year]\n a series' syllabus for a year: that year's where held,\n else the newest, saying which and what it is read from\n resolve comp [<feis> <code>] [--year <y>] [--title <t>]\n resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]\n [--order <o>] [--as-of <date>] [--population girls|boys]\n resolve registrations\n JSONL out, one answer per line; with no positional the\n questions are JSONL on stdin, one per line (a comp:\n {feis, code, year?, title?}, a dancer: {name, school?, age?,\n population?}, a registration: {name, school?, codes[], feis,\n year?, population?})\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
3
+ export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]\n\n config repo <owner/name> set the user-local repository the releases come from\n list [--remote] the cached releases (size, files, indexes, the pin), and\n with --remote the published tags\n fetch [--release <spec>] [--assets results,majors,<file>...]\n download, verify and cache a release (default: results)\n pin <spec> the user-local pin: what a bare open (spec \"pinned\") opens\n unpin remove the pin (a bare open is then \"latest\")\n current the pin, and whether it is cached\n add <dir> --tag <tag> add a release built locally (its MANIFEST.json and files)\n to the cache under its tag\n index [--release <spec>] [<name>...] [--force]\n build the per-tag indexes (default: every one)\n verify [--release <spec>] re-hash every cached file against its receipt\n capabilities [--release <spec>] what the release carries of what the client reads, from\n its own schema: each feature full or missing (its calls\n refuse, naming the first cut that publishes it)\n prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)\n and the pinned one\n rm <tag> [--dry-run] remove one cached release (not the pinned one)\n drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)\n feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]\n the feises a query names\n series [--release <spec>] <name|host code|feis id>\n the series a name, a host code (fweb:ABC) or any feis id\n names: its years, hosts, codes and next instance\n school [--release <spec>] <print|school_id> [--region <code>]\n a school print or id to the published schools\n dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>\n [--order surname-first|given-first] [--school <print|id>]\n [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]\n [--population girls|boys] [--rows]\n the dancers a name reaches, each with its evidence\n (name level, age, school, population) and the arm; the results of\n the one dancer where one answers (or --rows for each)\n results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]\n [--school <print|id>] [--year <y>] [--limit <n>]\n the published result rows, prints and ids\n majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]\n the majors: every event and its years; an event's\n competitions; a competition's, an event's or a dancer's rows\n teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]\n the team result rows of an event-year; with --rosters its\n roster teams and their dancers; with --joins how each roster\n team meets the results\n events [--release <spec>] [--event <e>] [--year <y>]\n each event-year's dates and place\n teacher [--release <spec>] <name|TD number>\n the staff lists' people: where they taught, the register,\n where they sat\n adjudicator [--release <spec>] <name|TD number>\n the register's and the panels' people: the same answer\n comp [--release <spec>] <feis> <code> [--year <y>] [--title <t>]\n a feis's own comp code to the comp it names, through that\n feis's syllabus (or the prior year's)\n syllabus [--release <spec>] <feis> [year]\n one feis-year's comps, in printed order\n syllabus [--release <spec>] --for <series> [year]\n a series' syllabus for a year: that year's where held,\n else the newest, saying which and what it is read from\n resolve comp [<feis> <code>] [--year <y>] [--title <t>]\n resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]\n [--order <o>] [--as-of <date>] [--population girls|boys]\n resolve registrations\n JSONL out, one answer per line; with no positional the\n questions are JSONL on stdin, one per line (a comp:\n {feis, code, year?, title?}, a dancer: {name, school?, age?,\n population?}, a registration: {name, school?, codes[], feis,\n year?, population?})\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
4
4
  export interface Parsed {
5
5
  positional: string[];
6
6
  flags: Map<string, string>;
@@ -5,7 +5,7 @@ export { NAME_LEVELS, type NameLevel, nameLevel } from "./dancers-names.js";
5
5
  export { type Comp, type CompAnswer, type CompKind, type CompQuery, CompResolver, type CompSource, type FeisPlace, type SyllabusAnswer, type SyllabusForAnswer, type SyllabusSource, type TitleCheck, canonicalOf, } from "./comps.js";
6
6
  export { type ClientOptions, cacheRoot, repoOf } from "./config.js";
7
7
  export { type Feis, type FeisAnswer, FeisFinder, type FeisQuery, NO_CALENDAR, type Series, type SeriesAnswer, type SeriesInstance, type SeriesNext, } from "./feises.js";
8
- export { type IndexBuilder, type IndexFile, buildIndex, defaultBuilders, indexKey, indexPath, isCurrent, listIndexes, } from "./indexes.js";
8
+ export { type IndexBuilder, type IndexFile, buildIndex, defaultBuilders, ensureIndex, indexKey, indexPath, isCurrent, listIndexes, } from "./indexes.js";
9
9
  export { type CachedRelease, type ReleaseList, ReleaseManager, type RemovalPlan, releases, } from "./manager.js";
10
10
  export { type Registered, type Sat, type StaffAnswer, StaffFinder, type StaffPerson, type Taught, type TdLink, } from "./people.js";
11
11
  export { type Pin, readPin } from "./pin.js";
@@ -15,4 +15,5 @@ export { type MajorCompetition, type MajorEvent, MajorsFinder, type ResultAnswer
15
15
  export { type DancerAnswerLike, type DancerQueryLike, type DancerResolverLike, type DancerUnavailable, type RegistrationRow, type ResolvedDancer, RegistrationResolver, type ResolvedRegistration, dancerResolverOf, youngest, } from "./registrations.js";
16
16
  export { type SchoolAnswer, type SchoolCandidate, type SchoolQuery, SchoolResolver, type SchoolRung, } from "./schools.js";
17
17
  export { type Capability, FEATURES, type Feature, type SchemaProbe, capabilityOf, columnsOf, needsRelease, } from "./schema.js";
18
+ export { EventFinder, type EventYear, type EventYearQuery, type ResultRoster, type RosterMember, type RosterTeam, TeamFinder, type TeamResult, } from "./teams.js";
18
19
  export { ARCHIVE_TAG, type ArchiveTag, type ReleaseSpec, compareTagsNewestFirst, cutOf, parseArchiveTag, parseReleaseSpec, pickTag, renderArchiveTag, } from "./tag.js";
@@ -6,7 +6,8 @@ export interface IndexBuilder {
6
6
  readonly inputs: readonly string[];
7
7
  readonly says: string;
8
8
  keyParts?(rel: Release): string[];
9
- build(rel: Release, out: Database): void;
9
+ load?(rel: Release): Promise<unknown>;
10
+ build(rel: Release, out: Database, loaded?: unknown): void;
10
11
  }
11
12
  export declare function defaultBuilders(): IndexBuilder[];
12
13
  export declare const indexDir: (rel: {
@@ -21,7 +22,12 @@ export declare function isCurrent(rel: Release, b: IndexBuilder): boolean;
21
22
  export declare function buildIndex(rel: Release, b: IndexBuilder, o: {
22
23
  force: boolean;
23
24
  log?: (line: string) => void;
25
+ loaded?: unknown;
24
26
  }): string;
27
+ export declare function ensureIndex(rel: Release, b: IndexBuilder, o: {
28
+ force: boolean;
29
+ log?: (line: string) => void;
30
+ }): Promise<string>;
25
31
  export interface IndexFile {
26
32
  name: string;
27
33
  path: string;
@@ -0,0 +1,4 @@
1
+ export declare const tableAsset: (table: string) => string;
2
+ export declare function plain(v: unknown): unknown;
3
+ export declare function readParquet(path: string, columns?: readonly string[]): Promise<Record<string, unknown>[]>;
4
+ export declare function parquetColumns(path: string): Promise<Set<string>>;
@@ -12,6 +12,7 @@ import { MajorsFinder, ResultFinder } from "./results.js";
12
12
  import { type Capability } from "./schema.js";
13
13
  import { type SchoolReaderChoice, SchoolResolver } from "./schools.js";
14
14
  import { type ReleaseSpec } from "./tag.js";
15
+ import { EventFinder, TeamFinder } from "./teams.js";
15
16
  export interface OpenOptions extends ClientOptions {
16
17
  assets?: readonly string[];
17
18
  remote?: Remote;
@@ -41,6 +42,8 @@ export declare class Release {
41
42
  readonly staff: StaffFinder;
42
43
  readonly comps: CompResolver;
43
44
  readonly registrations: RegistrationResolver;
45
+ readonly teams: TeamFinder;
46
+ readonly events: EventFinder;
44
47
  private readonly opts;
45
48
  private readonly handles;
46
49
  private remoteAssets;
@@ -14,6 +14,7 @@ export interface Capability extends Feature {
14
14
  export interface SchemaProbe {
15
15
  db(name: string): Promise<Database>;
16
16
  publishes(asset: string): Promise<boolean>;
17
+ tableColumns?(table: string): Promise<Set<string>>;
17
18
  }
18
19
  export declare function hasNeed(probe: SchemaProbe, n: string): Promise<boolean>;
19
20
  export declare function capabilityOf(probe: SchemaProbe, f: Feature): Promise<Capability>;
@@ -0,0 +1,105 @@
1
+ import type { IndexBuilder } from "./indexes.js";
2
+ import type { Release } from "./release.js";
3
+ export interface EventYearQuery {
4
+ event?: string;
5
+ year?: number;
6
+ }
7
+ export interface TeamResult {
8
+ address: string;
9
+ competition: string;
10
+ event: string;
11
+ year: number;
12
+ card: string | null;
13
+ comp_no: string | null;
14
+ title: string | null;
15
+ category: string | null;
16
+ hands: number | null;
17
+ letter: string | null;
18
+ label: string | null;
19
+ name_printed: string | null;
20
+ school_printed: string | null;
21
+ school_loc: string | null;
22
+ school_id: string | null;
23
+ place: number | null;
24
+ place_printed: string | null;
25
+ tie: number | null;
26
+ competitors: number | null;
27
+ atts: string[];
28
+ }
29
+ export interface RosterMember {
30
+ seq: number;
31
+ name_printed: string;
32
+ name: string | null;
33
+ yob: number | null;
34
+ dancer_id: number | null;
35
+ dancer_scope: "per-release" | "stable" | null;
36
+ }
37
+ export interface RosterTeam {
38
+ team: string;
39
+ kind: string;
40
+ event: string | null;
41
+ year: number;
42
+ comp_no: string | null;
43
+ title: string | null;
44
+ team_index: string | null;
45
+ seq: number | null;
46
+ school_id: string | null;
47
+ school_printed: string | null;
48
+ members: RosterMember[];
49
+ }
50
+ export interface ResultRoster {
51
+ team: string;
52
+ event: string | null;
53
+ year: number | null;
54
+ comp_no: string | null;
55
+ school_id: string | null;
56
+ result_address: string | null;
57
+ join_kind: string;
58
+ via_split: boolean;
59
+ roster_teams: number | null;
60
+ result_teams: number | null;
61
+ roster_index: string | null;
62
+ result_letter: string | null;
63
+ }
64
+ export interface EventYear {
65
+ event: string;
66
+ year: number;
67
+ date_lo: string | null;
68
+ date_hi: string | null;
69
+ resolution: string | null;
70
+ source: string | null;
71
+ date_printed: string[];
72
+ place: string | null;
73
+ place_printed: string[];
74
+ }
75
+ export declare const teamsIndex: IndexBuilder;
76
+ export declare const eventsIndex: IndexBuilder;
77
+ export declare class TeamFinder {
78
+ private readonly rel;
79
+ constructor(rel: Release);
80
+ private db;
81
+ results(q?: EventYearQuery): Promise<{
82
+ tag: string;
83
+ asked: EventYearQuery;
84
+ results: TeamResult[];
85
+ }>;
86
+ rosters(q?: EventYearQuery): Promise<{
87
+ tag: string;
88
+ asked: EventYearQuery;
89
+ teams: RosterTeam[];
90
+ }>;
91
+ resultRosters(q?: EventYearQuery): Promise<{
92
+ tag: string;
93
+ asked: EventYearQuery;
94
+ joins: ResultRoster[];
95
+ }>;
96
+ }
97
+ export declare class EventFinder {
98
+ private readonly rel;
99
+ constructor(rel: Release);
100
+ years(q?: EventYearQuery): Promise<{
101
+ tag: string;
102
+ asked: EventYearQuery;
103
+ years: EventYear[];
104
+ }>;
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Open a versioned release of an Irish dance results archive: download, verify, decompress and cache it by tag, keep the cache, and resolve feises and schools against it",
5
5
  "keywords": ["irish-dance", "results", "release", "cache", "sqlite"],
6
6
  "license": "MIT",
@@ -45,7 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@finbheara/domain": "0.49.0",
48
- "@finbheara/names": "0.22.0"
48
+ "@finbheara/names": "0.22.0",
49
+ "hyparquet": "1.31.2"
49
50
  },
50
51
  "devDependencies": {
51
52
  "typescript": "7.0.2"