@finbheara/archive-client 0.4.0 → 0.6.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
@@ -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.4.x"`).
23
+ (`"@finbheara/archive-client": "0.6.x"`).
24
24
 
25
25
  ## Configuration
26
26
 
@@ -85,7 +85,13 @@ 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
+ | `results.stream`, `majors.stream` | `results.stream`, `results.entries` | 255 |
89
+ | `feises.dates` | `feises.dates` | 245 |
90
+ | `dancers.records` | `dancers.records` | 215 |
91
+ | `dancers.years` | `dancers.years` | 245 |
92
+ | `majors.tabs` | `majors.tabulated` | 215 |
88
93
  | `teams` | `teams.results`, `teams.rosters`, `teams.resultRosters` | 260 |
94
+ | `teams.fields` | the fields `teams.results` (`age`, `gender`, `printed_status`) and `teams.rosters` (`dance`, `school_printed_as_of`, a member's `gender`) carry; absent, never refused, without it | 266 |
89
95
  | `events` | `events.years` | 260 |
90
96
 
91
97
  A spec is `pinned` (the default: the user-local pin, else the latest), `latest`, a cut number
@@ -276,15 +282,60 @@ A row carries the prints and the ids that stand for them: the address, the compe
276
282
  and age, the place, the name and school as printed with the dancer and school ids. The majors are
277
283
  read from `majors.sqlite`, fetched on first use.
278
284
 
285
+ ## Every row, streamed
286
+
287
+ ```ts
288
+ for await (const row of rel.results.stream({ scope: "majors" })) { ... }
289
+ for await (const row of rel.results.stream({
290
+ scope: "platform", // "majors" | "transcribed" | "platform" | "all"
291
+ primary: true, // only rows no other source stands in for
292
+ year: [2015, 2024], // a year or an inclusive range
293
+ feis_level: [600, 700], // also: event (one or several), tier, category, dancer_id, school_id
294
+ })) { ... }
295
+ for await (const entry of rel.results.entries({ scope: "all", event: "<event>" })) { ... }
296
+ ```
297
+
298
+ A stream yields one row at a time and never holds the table in memory. A row carries its
299
+ competition beside its own fields: `competition`, `event`, `year`, `tier`, `comp_no`, `title`,
300
+ `category`, `age`, `age_printed`, `gender`, `competitors`, `feis_level`, `level_printed`,
301
+ `date_printed`, `source`, `round`, and at a feis its `feis_id`, `feis_date_lo` and
302
+ `feis_date_hi`. Its own: `address`, `section`, `card`, `place`, `place_printed`, `tie`,
303
+ `relative_place`, `atts`, `withdrawn`, `qualifier`, `name_printed`, `name`, `school_printed`,
304
+ `school_loc`, `school_id`, `dancer_id`, `dancer_scope`, `yob`, `yob_printed`, `points_printed`,
305
+ `platform_dancer_id`, `primary_source`, and its key in the release, `file` and `line` (one
306
+ `(file, line)` is one row, where an `address` may carry one row per section). Rows come in the
307
+ release's own order, a competition's rows together.
308
+
309
+ The scopes: `majors` reads the majors (tiers T1 to T3), `transcribed` the transcribed rows of
310
+ every tier, `platform` every row a results platform published, `all` every row.
311
+
312
+ AN ENTRY is one row per address, the row the card stands for: a card printed in several sections
313
+ (a recall order, the final, a single round's solo placing, a qualifying heat) keeps the final's
314
+ row, then the sectionless page's, the recall order's, the standings', the unplaced row, a heat's
315
+ and last a solo round's (`entryRank`), and two rows of one rank keep the later. `recall_place` is
316
+ the place the address's recall row carries.
317
+
318
+ ## Bulk reads
319
+
320
+ ```ts
321
+ await rel.dancers.records(); // every dancer: display, appearances, birth_lo, birth_hi,
322
+ // birth_source, birth_conflict, genders, scope
323
+ await rel.dancers.records({ ids: [1001] });
324
+ await rel.dancers.years({ ids: [1001], year: 2025 }); // region, region_source, nationals, geohash
325
+ await rel.feises.dates(); // every feis id with its first and last day
326
+ await rel.majors.tabulated({ min_true_round: 3 }); // the majors addresses marked to round 3 or later
327
+ ```
328
+
279
329
  ## Teams and event-years
280
330
 
281
331
  ```ts
282
332
  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, ... }]
333
+ r.results; // [{ address, competition, event, year, comp_no, title, category, hands, age, gender,
334
+ // letter, label, school_printed, school_id, place, place_printed, printed_status, tie,
335
+ // competitors, atts, ... }]
285
336
  const t = await rel.teams.rosters({ event: "<event>", year: 2024 });
286
337
  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 }] }]
338
+ // school_printed_as_of, dance, members: [{ seq, name_printed, name, yob, gender, dancer_id, dancer_scope }] }]
288
339
  const j = await rel.teams.resultRosters({ event: "<event>", year: 2024 });
289
340
  j.joins; // [{ team, result_address, join_kind, via_split, roster_index, result_letter, ... }]
290
341
  const e = await rel.events.years({ event: "<event>" });
@@ -299,7 +350,21 @@ roster team met the results (`exact`, `card`, `branch`) or why it did not (`no-r
299
350
  `no-school`, `no-roster`, `multi-team-unjoined`), and `result_address` is null where none joined.
300
351
  An event-year's dates are a window (`date_lo`, `date_hi`) with how fine it is (`resolution`:
301
352
  `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
353
+ where the release knows one. A team result's `age` and `gender` are the competition's band and sex
354
+ as the release reads them, `printed_status` the row's printed status cell verbatim. A roster
355
+ team's `school_id` is its members' own school that year. Its `school_printed` is the school a
356
+ registration system held for it on `school_printed_as_of`, often years later, so a rename or a move
357
+ since shows there and it says nothing about the school that year. Its `dance` is the dance it was
358
+ entered for, and a member's `gender` is printed only as `M`:
359
+ the source withholds every other print, so null says nothing about a dancer's sex. A field the
360
+ source left empty is null.
361
+
362
+ THOSE NEWER FIELDS ARE ABSENT, NEVER NULL, ON A RELEASE THAT DOES NOT CARRY THEM: a team result's
363
+ `age`, `gender`, `printed_status`, a roster team's `school_printed_as_of`, `dance` and a member's
364
+ `gender` (feature `teams.fields`, see the table above). Absent means "this release does not carry it", null
365
+ means the release carries the field and has no value. `teams.results` and `teams.rosters` answer on
366
+ every release the `teams` feature names, and `(await rel.capability("teams.fields")).state` says which shape
367
+ a release has. The rows are read once per
303
368
  release into the `teams` and `events` indexes.
304
369
 
305
370
  ## Teachers and adjudicators
@@ -426,6 +491,13 @@ and the competitions still resolve.
426
491
  [--population girls|boys] [--rows]
427
492
  idq results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <c>] [--school <s>] [--year <y>] [--limit <n>]
428
493
  idq majors [--release <spec>] [--event <e>] [--year <y>] [--comp <c>] [--dancer <id>] [--rows]
494
+ idq stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]
495
+ [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]
496
+ [--level <n,...>] [--dancer <id,...>] [--school <school_id>] [--primary]
497
+ idq records [--release <spec>] [--dancer <id,...>]
498
+ idq records --years [--release <spec>] [--dancer <id,...>] [--year <y>]
499
+ idq feis-dates [--release <spec>]
500
+ idq tabulated [--release <spec>] [--min-round <n>]
429
501
  idq teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]
430
502
  idq events [--release <spec>] [--event <e>] [--year <y>]
431
503
  idq teacher [--release <spec>] <name|TD number>
@@ -437,7 +509,7 @@ and the competitions still resolve.
437
509
  idq resolve dancer [<name>] [--school s] [--comp <feis>/<code> | --yob y | --band b --year y]
438
510
  idq resolve registrations
439
511
 
440
- `dancer` prints the results of the one dancer where the answer is unique (every candidate's with
512
+ `stream` prints one JSON line per row (or entry), as it reads them. `dancer` prints the results of the one dancer where the answer is unique (every candidate's with
441
513
  `--rows`). `--json` prints the call's answer as it is. `--cache` and `--repo` override the defaults.
442
514
 
443
515
  `resolve` speaks JSON lines: one answer per line out, and with no positional question the
@@ -106,8 +106,149 @@ function nameLevel2(q, c) {
106
106
  return null;
107
107
  }
108
108
 
109
+ var ENTRY_RANK = new Map([
110
+ ["final", 0],
111
+ ["", 1],
112
+ ["recall", 2],
113
+ ["standings", 3],
114
+ ["unplaced", 4],
115
+ ["qual", 5],
116
+ ["qual-a", 5],
117
+ ["qual-b", 5]
118
+ ]);
119
+ function entryRank2(section) {
120
+ const known = ENTRY_RANK.get(section);
121
+ if (known !== undefined)
122
+ return known;
123
+ if (/^solo(?:-r\d+)?$/.test(section))
124
+ return 6;
125
+ return 9;
126
+ }
127
+ var COLUMNS = `r.address AS address, r.competition AS competition, c.event AS event,
128
+ c.year AS year, c.tier AS tier, c.comp_no AS comp_no, c.title AS title, c.category AS category,
129
+ c.age AS age, c.age_printed AS age_printed, c.gender AS gender, c.competitors AS competitors,
130
+ c.feis_level AS feis_level, c.level_printed AS level_printed, c.date_printed AS date_printed,
131
+ c.source AS source, c.round AS round, r.section AS section, r.card AS card, r.place AS place,
132
+ r.place_printed AS place_printed, r.tie AS tie, r.relative_place AS relative_place,
133
+ r.atts AS atts, r.withdrawn AS withdrawn, r.qualifier AS qualifier,
134
+ r.name_printed AS name_printed, r.name AS name, r.school_printed AS school_printed,
135
+ r.school_loc AS school_loc, r.school_id AS school_id, r.dancer_id AS dancer_id, r.yob AS yob,
136
+ r.yob_printed AS yob_printed, r.points_printed AS points_printed,
137
+ r.platform_dancer_id AS platform_dancer_id, r.primary_source AS primary_source,
138
+ r.file AS file, r.line AS line`;
139
+ var TRANSCRIBED = "('transcript', 'manifested')";
140
+ var scopeDb = (s) => s === "majors" ? "majors" : "results";
141
+ function streamSql(q) {
142
+ const where = [];
143
+ const args = [];
144
+ const list = (col, v, nocase = false) => {
145
+ if (v.length === 0) {
146
+ where.push("0");
147
+ return;
148
+ }
149
+ where.push(`${col}${nocase ? " COLLATE NOCASE" : ""} IN (${v.map(() => "?").join(", ")})`);
150
+ args.push(...v);
151
+ };
152
+ if (q.scope === "transcribed")
153
+ where.push(`r.source IN ${TRANSCRIBED}`);
154
+ else if (q.scope === "platform")
155
+ where.push(`r.source NOT IN ${TRANSCRIBED}`);
156
+ else if (q.scope !== "majors" && q.scope !== "all")
157
+ throw new Error(`archive-client: no stream scope "${String(q.scope)}"`);
158
+ if (q.event !== undefined)
159
+ list("c.event", typeof q.event === "string" ? [q.event] : q.event, true);
160
+ if (typeof q.year === "number") {
161
+ where.push("c.year = ?");
162
+ args.push(q.year);
163
+ } else if (Array.isArray(q.year)) {
164
+ where.push("c.year BETWEEN ? AND ?");
165
+ args.push(q.year[0], q.year[1]);
166
+ }
167
+ if (q.tier !== undefined)
168
+ list("c.tier", q.tier);
169
+ if (q.category !== undefined)
170
+ list("c.category", q.category);
171
+ if (q.feis_level !== undefined)
172
+ list("c.feis_level", q.feis_level);
173
+ if (q.dancer_id !== undefined)
174
+ list("r.dancer_id", q.dancer_id);
175
+ if (q.school_id !== undefined) {
176
+ where.push("r.school_id = ?");
177
+ args.push(q.school_id);
178
+ }
179
+ if (q.primary === true)
180
+ where.push("(r.primary_source IS NULL OR r.primary_source = '')");
181
+ return {
182
+ sql: `SELECT ${COLUMNS} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
183
+ args
184
+ };
185
+ }
186
+ function feisDates(db) {
187
+ const out = new Map;
188
+ for (const f of db.query("SELECT feis_id, min(date_lo) AS lo, max(date_hi) AS hi FROM feises GROUP BY feis_id").all())
189
+ out.set(f.feis_id, { lo: f.lo, hi: f.hi });
190
+ return out;
191
+ }
192
+ var text = (v) => v === null || v === "" ? null : v;
193
+ function rowOf(r, dates) {
194
+ const { atts, withdrawn, ...rest } = r;
195
+ const d = dates.get(r.event);
196
+ return {
197
+ ...rest,
198
+ feis_id: d === undefined ? null : r.event,
199
+ feis_date_lo: d?.lo ?? null,
200
+ feis_date_hi: d?.hi ?? null,
201
+ atts: atts === "" ? [] : atts.split(","),
202
+ withdrawn: text(withdrawn),
203
+ qualifier: text(rest.qualifier),
204
+ primary_source: text(rest.primary_source),
205
+ dancer_scope: r.dancer_id === null ? null : r.dancer_id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable"
206
+ };
207
+ }
208
+ async function* streamRows(db, q) {
209
+ const dates = feisDates(db);
210
+ const { sql, args } = streamSql(q);
211
+ const stmt = db.prepare(sql);
212
+ try {
213
+ for (const r of stmt.iterate(...args))
214
+ yield rowOf(r, dates);
215
+ } finally {
216
+ stmt.finalize();
217
+ }
218
+ }
219
+ async function* entryRows(db, q) {
220
+ const dates = feisDates(db);
221
+ const { sql, args } = streamSql(q);
222
+ const stmt = db.prepare(sql);
223
+ let file = null;
224
+ let held = new Map;
225
+ const flush = function* () {
226
+ for (const h of held.values())
227
+ yield { ...rowOf(h.raw, dates), recall_place: h.recall };
228
+ held = new Map;
229
+ };
230
+ try {
231
+ for (const r of stmt.iterate(...args)) {
232
+ if (r.file !== file) {
233
+ yield* flush();
234
+ file = r.file;
235
+ }
236
+ const rank = entryRank2(r.section);
237
+ const h = held.get(r.address);
238
+ const recall = r.section === "recall" ? r.place : h?.recall ?? null;
239
+ if (h === undefined || rank <= h.rank)
240
+ held.set(r.address, { raw: r, rank, recall });
241
+ else
242
+ h.recall = recall;
243
+ }
244
+ yield* flush();
245
+ } finally {
246
+ stmt.finalize();
247
+ }
248
+ }
249
+
109
250
  var FEIS_LOCAL_ID_FLOOR2 = 17179869184;
110
- var COLUMNS = `r.address AS address, r.competition AS competition, c.event AS event, c.year AS year,
251
+ var COLUMNS2 = `r.address AS address, r.competition AS competition, c.event AS event, c.year AS year,
111
252
  c.tier AS tier, c.title AS title, c.age AS age, c.category AS category, c.gender AS gender,
112
253
  c.feis_level AS feis_level, c.comp_no AS comp_no, r.place AS place,
113
254
  r.place_printed AS place_printed, c.competitors AS competitors, r.name_printed AS name_printed,
@@ -139,7 +280,7 @@ function queryResults(rel, db, q) {
139
280
  args.push(q.year);
140
281
  }
141
282
  const limit = q.limit ?? 500;
142
- const all = rel.db(db).query(`SELECT ${COLUMNS} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
283
+ const all = rel.db(db).query(`SELECT ${COLUMNS2} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
143
284
  WHERE ${where.join(" AND ")}
144
285
  ORDER BY c.year, c.event, r.competition, r.place IS NULL, r.place, r.address`).all(...args);
145
286
  return {
@@ -164,6 +305,18 @@ class ResultFinder2 {
164
305
  await this.rel.require("results.level");
165
306
  return queryResults(this.rel, "results", q);
166
307
  }
308
+ async streamDb(q) {
309
+ const db = scopeDb(q.scope);
310
+ await this.rel.fetch(db);
311
+ await this.rel.require(db === "majors" ? "majors.stream" : "results.stream");
312
+ return this.rel.db(db);
313
+ }
314
+ async* stream(q) {
315
+ yield* streamRows(await this.streamDb(q), q);
316
+ }
317
+ async* entries(q) {
318
+ yield* entryRows(await this.streamDb(q), q);
319
+ }
167
320
  }
168
321
 
169
322
  class MajorsFinder2 {
@@ -208,6 +361,13 @@ class MajorsFinder2 {
208
361
  ORDER BY c.year, c.seq, c.competition`).all(...args).map((c) => ({ ...c, rows: rows.get(c.competition) ?? 0 }));
209
362
  return { tag: this.rel.tag, competitions };
210
363
  }
364
+ async tabulated(q = {}) {
365
+ const db = await this.db();
366
+ await this.rel.require("majors.tabs");
367
+ const min = q.min_true_round ?? 3;
368
+ const addresses = db.query("SELECT DISTINCT address FROM tabs WHERE true_round >= ? ORDER BY address").all(min).map((r) => r.address);
369
+ return { tag: this.rel.tag, min_true_round: min, addresses };
370
+ }
211
371
  async results(q) {
212
372
  await this.db();
213
373
  await this.rel.require("results.level");
@@ -365,6 +525,12 @@ var SCHOOL_WEIGHT = {
365
525
  unresolved: 0,
366
526
  unasked: 0
367
527
  };
528
+ function idChunks(ids) {
529
+ const out = [];
530
+ for (let i = 0;i < ids.length; i += 500)
531
+ out.push(ids.slice(i, i + 500));
532
+ return out;
533
+ }
368
534
 
369
535
  class DancerResolver2 {
370
536
  rel;
@@ -372,6 +538,34 @@ class DancerResolver2 {
372
538
  constructor(rel) {
373
539
  this.rel = rel;
374
540
  }
541
+ async records(q = {}) {
542
+ await this.rel.fetch("results");
543
+ await this.rel.require("dancers.records");
544
+ const db = this.rel.db("results");
545
+ const sql = "SELECT dancer_id, display, appearances, birth_lo, birth_hi, birth_source, birth_conflict, genders FROM dancers";
546
+ const rows = q.ids === undefined ? db.query(`${sql} ORDER BY dancer_id`).all() : idChunks(q.ids).flatMap((c) => db.query(`${sql} WHERE dancer_id IN (${c.map(() => "?").join(",")})`).all(...c));
547
+ rows.sort((a, b) => a.dancer_id - b.dancer_id);
548
+ return {
549
+ tag: this.rel.tag,
550
+ records: rows.map((r) => ({
551
+ ...r,
552
+ scope: r.dancer_id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable",
553
+ birth_conflict: r.birth_conflict === 1,
554
+ genders: r.genders === "" ? [] : r.genders.split(",")
555
+ }))
556
+ };
557
+ }
558
+ async years(q = {}) {
559
+ await this.rel.fetch("results");
560
+ await this.rel.require("dancers.years");
561
+ const db = this.rel.db("results");
562
+ const sql = "SELECT dancer_id, year, region, region_source, nationals, nationals_source, geohash FROM dancer_years";
563
+ const yearSql = q.year === undefined ? "" : " AND year = ?";
564
+ const yearArgs = q.year === undefined ? [] : [q.year];
565
+ const rows = q.ids === undefined ? db.query(`${sql} WHERE 1${yearSql} ORDER BY dancer_id, year`).all(...yearArgs) : idChunks(q.ids).flatMap((c) => db.query(`${sql} WHERE dancer_id IN (${c.map(() => "?").join(",")})${yearSql}`).all(...c, ...yearArgs));
566
+ rows.sort((a, b) => a.dancer_id - b.dancer_id || a.year - b.year);
567
+ return { tag: this.rel.tag, years: rows };
568
+ }
375
569
  async resolve(q) {
376
570
  const db = await this.rel.indexDb("dancers");
377
571
  const asked = dancerName(q.name, q.order === "unknown" ? undefined : q.order);
@@ -1067,6 +1261,12 @@ class FeisFinder2 {
1067
1261
  constructor(rel) {
1068
1262
  this.rel = rel;
1069
1263
  }
1264
+ async dates() {
1265
+ await this.rel.fetch("results");
1266
+ await this.rel.require("feises.dates");
1267
+ const dates = this.rel.db("results").query("SELECT feis_id, min(date_lo) AS date_lo, max(date_hi) AS date_hi FROM feises GROUP BY feis_id ORDER BY feis_id").all();
1268
+ return { tag: this.rel.tag, dates };
1269
+ }
1070
1270
  async find(q) {
1071
1271
  const db = await this.rel.indexDb("feises");
1072
1272
  const where = [];
@@ -1634,12 +1834,13 @@ CREATE TABLE event_year(event_year TEXT PRIMARY KEY, event TEXT NOT NULL, year I
1634
1834
  CREATE TABLE result(address TEXT PRIMARY KEY, competition TEXT, event TEXT, year INTEGER,
1635
1835
  card TEXT, comp_no TEXT, title TEXT, category TEXT, hands INTEGER, letter TEXT, label TEXT,
1636
1836
  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);
1837
+ place_printed TEXT, tie INTEGER, competitors INTEGER, atts TEXT, age TEXT, gender TEXT,
1838
+ printed_status TEXT, ord INTEGER);
1638
1839
  CREATE TABLE roster_team(team TEXT PRIMARY KEY, kind TEXT, event TEXT, year INTEGER,
1639
1840
  comp_no TEXT, title TEXT, team_index TEXT, seq INTEGER, school_id TEXT, school_printed TEXT,
1640
- ord INTEGER);
1841
+ dance TEXT, school_printed_as_of TEXT, ord INTEGER);
1641
1842
  CREATE TABLE roster_member(team TEXT, seq INTEGER, name_printed TEXT, name TEXT, yob INTEGER,
1642
- dancer_id INTEGER, ord INTEGER);
1843
+ dancer_id INTEGER, gender TEXT, ord INTEGER);
1643
1844
  CREATE TABLE result_roster(team TEXT, event TEXT, year INTEGER, comp_no TEXT, school_id TEXT,
1644
1845
  result_address TEXT, join_kind TEXT, via_split INTEGER, roster_teams INTEGER,
1645
1846
  result_teams INTEGER, roster_index TEXT, result_letter TEXT, ord INTEGER);
@@ -1663,7 +1864,7 @@ async function readTables(rel, tables) {
1663
1864
  }
1664
1865
  var teamsIndex = {
1665
1866
  name: "teams",
1666
- version: "1",
1867
+ version: "2",
1667
1868
  inputs: TEAM_TABLES.map(tableAsset),
1668
1869
  says: "the team results, the roster teams with their members, and how they join, by event-year",
1669
1870
  load: (rel) => readTables(rel, TEAM_TABLES),
@@ -1681,18 +1882,18 @@ var teamsIndex = {
1681
1882
  ey.set(k, { event, year });
1682
1883
  putEy.run(k, event, year);
1683
1884
  }
1684
- const putR = out.prepare("INSERT OR IGNORE INTO result VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1885
+ const putR = out.prepare("INSERT OR IGNORE INTO result VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1685
1886
  let ord = 0;
1686
1887
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1888
+ 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++);
1889
+ const putT = out.prepare("INSERT OR IGNORE INTO roster_team VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1689
1890
  for (const r of t.roster_teams) {
1690
1891
  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++);
1892
+ 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++);
1692
1893
  }
1693
- const putM = out.prepare("INSERT INTO roster_member VALUES (?, ?, ?, ?, ?, ?, ?)");
1894
+ const putM = out.prepare("INSERT INTO roster_member VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
1694
1895
  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++);
1896
+ 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++);
1696
1897
  const putJ = out.prepare("INSERT INTO result_roster VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1697
1898
  for (const r of t.team_result_rosters) {
1698
1899
  const e = ey.get(str(r.event_year) ?? "");
@@ -1747,27 +1948,37 @@ class TeamFinder2 {
1747
1948
  db() {
1748
1949
  return this.rel.indexDb("teams");
1749
1950
  }
1951
+ async hasFields() {
1952
+ return (await this.rel.capability("teams.fields")).state === "full";
1953
+ }
1750
1954
  async results(q = {}) {
1751
1955
  const db = await this.db();
1956
+ const fields = await this.hasFields();
1752
1957
  const w = whereOf(q);
1753
1958
  const rows = db.query(`SELECT address, competition, event, year, card, comp_no, title, category, hands, letter,
1754
1959
  label, name_printed, school_printed, school_loc, school_id, place, place_printed, tie,
1755
- competitors, atts
1960
+ competitors, atts${fields ? ", age, gender, printed_status" : ""}
1756
1961
  FROM result${w.sql} ORDER BY year, event, competition, place IS NULL, place, ord`).all(...w.args);
1757
1962
  return {
1758
1963
  tag: this.rel.tag,
1759
1964
  asked: q,
1760
- results: rows.map((r) => ({ ...r, atts: JSON.parse(r.atts) }))
1965
+ results: rows.map(({ printed_status, ...r }) => ({
1966
+ ...r,
1967
+ atts: JSON.parse(r.atts),
1968
+ ...printed_status === undefined ? {} : { printed_status: JSON.parse(printed_status ?? "[]") }
1969
+ }))
1761
1970
  };
1762
1971
  }
1763
1972
  async rosters(q = {}) {
1764
1973
  const db = await this.db();
1974
+ const fields = await this.hasFields();
1765
1975
  const w = whereOf(q, "t.");
1766
1976
  const teams = db.query(`SELECT team, kind, event, year, comp_no, title, team_index, seq, school_id, school_printed
1977
+ ${fields ? ", school_printed_as_of, dance" : ""}
1767
1978
  FROM roster_team t${w.sql} ORDER BY year, event, ord`).all(...w.args);
1768
1979
  const members = new Map;
1769
1980
  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
1981
+ m.yob AS yob, m.dancer_id AS dancer_id${fields ? ", m.gender AS gender" : ""}
1771
1982
  FROM roster_member m JOIN roster_team t ON t.team = m.team${w.sql}
1772
1983
  ORDER BY m.team, m.seq, m.ord`).all(...w.args)) {
1773
1984
  const { team, ...rest } = m;
@@ -2298,6 +2509,74 @@ var FEATURES2 = [
2298
2509
  since: 265,
2299
2510
  calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
2300
2511
  },
2512
+ {
2513
+ feature: "results.stream",
2514
+ needs: [
2515
+ "results:competitions.round",
2516
+ "results:competitions.feis_level",
2517
+ "results:competitions.level_printed",
2518
+ "results:competitions.age_printed",
2519
+ "results:results.yob_printed",
2520
+ "results:results.primary_source",
2521
+ "results:results.platform_dancer_id",
2522
+ "results:results.relative_place",
2523
+ "results:feises.date_lo",
2524
+ "results:feises.date_hi"
2525
+ ],
2526
+ since: 255,
2527
+ calls: "results.stream, results.entries (scopes transcribed, platform, all)"
2528
+ },
2529
+ {
2530
+ feature: "majors.stream",
2531
+ needs: [
2532
+ "majors:competitions.round",
2533
+ "majors:competitions.feis_level",
2534
+ "majors:results.yob_printed",
2535
+ "majors:results.primary_source",
2536
+ "majors:results.relative_place",
2537
+ "majors:feises.date_lo"
2538
+ ],
2539
+ since: 255,
2540
+ calls: "results.stream, results.entries (scope majors)"
2541
+ },
2542
+ {
2543
+ feature: "majors.tabs",
2544
+ needs: ["majors:tabs.address", "majors:tabs.true_round"],
2545
+ since: 215,
2546
+ calls: "majors.tabulated"
2547
+ },
2548
+ {
2549
+ feature: "feises.dates",
2550
+ needs: ["results:feises.feis_id", "results:feises.date_lo", "results:feises.date_hi"],
2551
+ since: 245,
2552
+ calls: "feises.dates"
2553
+ },
2554
+ {
2555
+ feature: "dancers.records",
2556
+ needs: [
2557
+ "results:dancers.display",
2558
+ "results:dancers.appearances",
2559
+ "results:dancers.birth_lo",
2560
+ "results:dancers.birth_hi",
2561
+ "results:dancers.birth_source",
2562
+ "results:dancers.birth_conflict",
2563
+ "results:dancers.genders"
2564
+ ],
2565
+ since: 215,
2566
+ calls: "dancers.records"
2567
+ },
2568
+ {
2569
+ feature: "dancers.years",
2570
+ needs: [
2571
+ "results:dancer_years.region",
2572
+ "results:dancer_years.region_source",
2573
+ "results:dancer_years.nationals",
2574
+ "results:dancer_years.nationals_source",
2575
+ "results:dancer_years.geohash"
2576
+ ],
2577
+ since: 245,
2578
+ calls: "dancers.years"
2579
+ },
2301
2580
  {
2302
2581
  feature: "teams",
2303
2582
  needs: [
@@ -2356,6 +2635,19 @@ var FEATURES2 = [
2356
2635
  since: 260,
2357
2636
  calls: "teams.results, teams.rosters, teams.resultRosters (the teams index)"
2358
2637
  },
2638
+ {
2639
+ feature: "teams.fields",
2640
+ needs: [
2641
+ "table:team_results.age",
2642
+ "table:team_results.gender",
2643
+ "table:team_results.printed_status",
2644
+ "table:roster_teams.dance",
2645
+ "table:roster_teams.school_print_as_of",
2646
+ "table:roster_members.gender"
2647
+ ],
2648
+ since: 266,
2649
+ calls: "the fields teams.results (age, gender, printed_status) and teams.rosters (dance, school_printed_as_of, a member's gender) carry where present: absent, never refused, on a release without them"
2650
+ },
2359
2651
  {
2360
2652
  feature: "events",
2361
2653
  needs: [
@@ -2929,4 +3221,4 @@ function releases2(opts = {}) {
2929
3221
  return new ReleaseManager2(opts);
2930
3222
  }
2931
3223
 
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 };
3224
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, entryRank2, 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-v8e0qe9f.js";
7
+ } from "./chunks/index-gy5xye67.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
@@ -50,6 +50,21 @@ 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
+ stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]
54
+ [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]
55
+ [--level <feis_level,...>] [--dancer <id,...>] [--school <school_id>] [--primary]
56
+ every row a scope holds, one JSON line each, never the
57
+ whole table in memory; --entries one per address (the row
58
+ it stands for, with its recall order)
59
+ records [--release <spec>] [--dancer <id,...>]
60
+ every dancer's record: display, appearances, birth window,
61
+ populations
62
+ records --years [--release <spec>] [--dancer <id,...>] [--year <y>]
63
+ each dancer's region, nationals and place per year
64
+ feis-dates [--release <spec>] every feis's first and last day
65
+ tabulated [--release <spec>] [--min-round <n>]
66
+ the majors addresses the marks sheets tabulate to round n
67
+ or later (default 3)
53
68
  teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]
54
69
  the team result rows of an event-year; with --rosters its
55
70
  roster teams and their dancers; with --joins how each roster
@@ -107,7 +122,12 @@ var VALUE = new Set([
107
122
  "--limit",
108
123
  "--event",
109
124
  "--title",
110
- "--for"
125
+ "--for",
126
+ "--scope",
127
+ "--tier",
128
+ "--category",
129
+ "--level",
130
+ "--min-round"
111
131
  ]);
112
132
  function parseArgs(argv) {
113
133
  const positional = [];
@@ -319,6 +339,45 @@ async function resolveVerb(rel, what, args, p, say, stdin) {
319
339
  throw new Error("resolve comp|dancer|registrations");
320
340
  }
321
341
  }
342
+ var SCOPES = ["majors", "transcribed", "platform", "all"];
343
+ var listOf = (v) => v.split(",").map((x) => x.trim()).filter((x) => x !== "");
344
+ var idsOf = (v) => v === undefined ? undefined : listOf(v).map(Number);
345
+ function streamQueryOf(p) {
346
+ const scope = p.flags.get("--scope");
347
+ if (scope === undefined || !SCOPES.includes(scope))
348
+ throw new Error(`stream takes --scope ${SCOPES.join("|")}`);
349
+ const q = { scope };
350
+ const event = p.flags.get("--event");
351
+ if (event !== undefined)
352
+ q.event = listOf(event);
353
+ const year = p.flags.get("--year");
354
+ if (year !== undefined) {
355
+ const m = /^(\d{4})\.\.(\d{4})$/.exec(year);
356
+ q.year = m === null ? Number(year) : [Number(m[1]), Number(m[2])];
357
+ if (typeof q.year === "number" && !Number.isInteger(q.year))
358
+ throw new Error(`--year takes a year or <from>..<to>, not "${year}"`);
359
+ }
360
+ for (const [flag, key] of [
361
+ ["--tier", "tier"],
362
+ ["--category", "category"]
363
+ ]) {
364
+ const v = p.flags.get(flag);
365
+ if (v !== undefined)
366
+ q[key] = listOf(v);
367
+ }
368
+ const level = p.flags.get("--level");
369
+ if (level !== undefined)
370
+ q.feis_level = listOf(level).map(Number);
371
+ const dancers = idsOf(p.flags.get("--dancer"));
372
+ if (dancers !== undefined)
373
+ q.dancer_id = dancers;
374
+ const school = p.flags.get("--school");
375
+ if (school !== undefined)
376
+ q.school_id = school;
377
+ if (p.bools.has("--primary"))
378
+ q.primary = true;
379
+ return q;
380
+ }
322
381
  function eventYearOf(p) {
323
382
  const event = p.flags.get("--event");
324
383
  const year = p.flags.get("--year");
@@ -644,6 +703,79 @@ async function main(argv, io = {}) {
644
703
  rel.close();
645
704
  }
646
705
  }
706
+ case "stream": {
707
+ const q = streamQueryOf(p);
708
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
709
+ try {
710
+ let n = 0;
711
+ const rows = p.bools.has("--entries") ? rel.results.entries(q) : rel.results.stream(q);
712
+ for await (const r of rows) {
713
+ say(JSON.stringify(r));
714
+ n++;
715
+ }
716
+ log(`${n} ${p.bools.has("--entries") ? "entries" : "rows"} in ${rel.tag}`);
717
+ return n === 0 ? 1 : 0;
718
+ } finally {
719
+ rel.close();
720
+ }
721
+ }
722
+ case "records": {
723
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
724
+ try {
725
+ const ids = idsOf(p.flags.get("--dancer"));
726
+ if (p.bools.has("--years")) {
727
+ const year = p.flags.get("--year");
728
+ const a = await rel.dancers.years({
729
+ ...ids !== undefined ? { ids } : {},
730
+ ...year !== undefined ? { year: Number(year) } : {}
731
+ });
732
+ out(a, () => {
733
+ for (const y of a.years)
734
+ say(` ${y.dancer_id} ${y.year} ${y.region ?? "-"} (${y.region_source ?? "-"}) nationals ${y.nationals ?? "-"} ${y.geohash ?? "-"}`);
735
+ say(`${a.years.length} dancer-year(s) in ${a.tag}`);
736
+ });
737
+ return a.years.length === 0 ? 1 : 0;
738
+ }
739
+ const a = await rel.dancers.records(ids !== undefined ? { ids } : {});
740
+ out(a, () => {
741
+ for (const r of a.records)
742
+ say(` ${r.dancer_id} ${r.display} ${r.appearances} entries born ${r.birth_lo ?? "?"}..${r.birth_hi ?? "?"} (${r.birth_source}${r.birth_conflict ? ", conflict" : ""}) ${r.genders.join(",") || "-"}`);
743
+ say(`${a.records.length} dancer(s) in ${a.tag}`);
744
+ });
745
+ return a.records.length === 0 ? 1 : 0;
746
+ } finally {
747
+ rel.close();
748
+ }
749
+ }
750
+ case "feis-dates": {
751
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
752
+ try {
753
+ const a = await rel.feises.dates();
754
+ out(a, () => {
755
+ for (const d of a.dates)
756
+ say(` ${d.feis_id} ${d.date_lo ?? "?"}..${d.date_hi ?? "?"}`);
757
+ say(`${a.dates.length} feis(es) in ${a.tag}`);
758
+ });
759
+ return a.dates.length === 0 ? 1 : 0;
760
+ } finally {
761
+ rel.close();
762
+ }
763
+ }
764
+ case "tabulated": {
765
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
766
+ try {
767
+ const min = p.flags.get("--min-round");
768
+ const a = await rel.majors.tabulated(min !== undefined ? { min_true_round: Number(min) } : {});
769
+ out(a, () => {
770
+ for (const x of a.addresses)
771
+ say(` ${x}`);
772
+ say(`${a.addresses.length} address(es) tabulated to round ${a.min_true_round} or later in ${a.tag}`);
773
+ });
774
+ return a.addresses.length === 0 ? 1 : 0;
775
+ } finally {
776
+ rel.close();
777
+ }
778
+ }
647
779
  case "teams": {
648
780
  const rel = await openRelease2(spec, { ...opts, assets: [] });
649
781
  try {
@@ -783,5 +915,6 @@ export {
783
915
  USAGE,
784
916
  jsonLines,
785
917
  main,
786
- parseArgs
918
+ parseArgs,
919
+ streamQueryOf
787
920
  };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  unzstdTo2,
7
7
  NAME_LEVELS2,
8
8
  nameLevel2,
9
+ entryRank2,
9
10
  FEIS_LOCAL_ID_FLOOR2,
10
11
  ResultFinder2,
11
12
  MajorsFinder2,
@@ -44,7 +45,7 @@ import {
44
45
  openCached2,
45
46
  ReleaseManager2,
46
47
  releases2
47
- } from "./chunks/index-v8e0qe9f.js";
48
+ } from "./chunks/index-gy5xye67.js";
48
49
  import {
49
50
  cacheRoot2,
50
51
  repoOf2
@@ -87,6 +88,7 @@ export {
87
88
  dancerResolverOf2 as dancerResolverOf,
88
89
  defaultBuilders2 as defaultBuilders,
89
90
  ensureIndex2 as ensureIndex,
91
+ entryRank2 as entryRank,
90
92
  fallbackRemote2 as fallbackRemote,
91
93
  ghRemote2 as ghRemote,
92
94
  httpsRemote2 as httpsRemote,
@@ -1,6 +1,7 @@
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 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)";
3
+ import type { StreamQuery } from "./stream.js";
4
+ 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 stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]\n [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]\n [--level <feis_level,...>] [--dancer <id,...>] [--school <school_id>] [--primary]\n every row a scope holds, one JSON line each, never the\n whole table in memory; --entries one per address (the row\n it stands for, with its recall order)\n records [--release <spec>] [--dancer <id,...>]\n every dancer's record: display, appearances, birth window,\n populations\n records --years [--release <spec>] [--dancer <id,...>] [--year <y>]\n each dancer's region, nationals and place per year\n feis-dates [--release <spec>] every feis's first and last day\n tabulated [--release <spec>] [--min-round <n>]\n the majors addresses the marks sheets tabulate to round n\n or later (default 3)\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
5
  export interface Parsed {
5
6
  positional: string[];
6
7
  flags: Map<string, string>;
@@ -15,6 +16,7 @@ type Line<T> = {
15
16
  error: string;
16
17
  };
17
18
  export declare function jsonLines<T>(text: string, check: (v: T) => string | null): Line<T>[];
19
+ export declare function streamQueryOf(p: Parsed): StreamQuery;
18
20
  export declare function main(argv: readonly string[], io?: {
19
21
  say?: (s: string) => void;
20
22
  log?: (s: string) => void;
@@ -140,10 +140,43 @@ export interface DancerRecord {
140
140
  export declare const PACKAGE_KEY_PARTS: readonly string[];
141
141
  export declare const dancersIndex: IndexBuilder;
142
142
  export declare function yearOf(asOf: string | undefined): number | null;
143
+ export interface DancerBulkRecord {
144
+ dancer_id: number;
145
+ display: string;
146
+ scope: "per-release" | "stable";
147
+ appearances: number;
148
+ birth_lo: number | null;
149
+ birth_hi: number | null;
150
+ birth_source: string;
151
+ birth_conflict: boolean;
152
+ genders: string[];
153
+ }
154
+ export interface DancerYear {
155
+ dancer_id: number;
156
+ year: number;
157
+ region: string | null;
158
+ region_source: string | null;
159
+ nationals: string | null;
160
+ nationals_source: string | null;
161
+ geohash: string | null;
162
+ }
143
163
  export declare class DancerResolver {
144
164
  private readonly rel;
145
165
  private readonly printIds;
146
166
  constructor(rel: Release);
167
+ records(q?: {
168
+ ids?: readonly number[];
169
+ }): Promise<{
170
+ tag: string;
171
+ records: DancerBulkRecord[];
172
+ }>;
173
+ years(q?: {
174
+ ids?: readonly number[];
175
+ year?: number;
176
+ }): Promise<{
177
+ tag: string;
178
+ years: DancerYear[];
179
+ }>;
147
180
  resolve(q: DancerQuery): Promise<DancerAnswer>;
148
181
  search(q: DancerSearchQuery): Promise<DancerAnswer>;
149
182
  private capped;
@@ -77,6 +77,14 @@ export declare const NO_CALENDAR = "the release holds a feis-year once its resul
77
77
  export declare class FeisFinder {
78
78
  private readonly rel;
79
79
  constructor(rel: Release);
80
+ dates(): Promise<{
81
+ tag: string;
82
+ dates: {
83
+ feis_id: string;
84
+ date_lo: string | null;
85
+ date_hi: string | null;
86
+ }[];
87
+ }>;
80
88
  find(q: FeisQuery): Promise<FeisAnswer>;
81
89
  codeStem(token: string, stem: string): Promise<string[]>;
82
90
  seriesOf(brand: string): Promise<Series>;
@@ -1,6 +1,6 @@
1
1
  export { type AssetPlan, DATABASES, type DatabaseName, type LandedFile, type Receipt, type ReleaseManifest, planAsset, } from "./assets.js";
2
2
  export { sha256File, unzstdTo } from "./bytes.js";
3
- export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerSchool, type DancerSearchQuery, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type PopulationEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
3
+ export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerBulkRecord, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerSchool, type DancerSearchQuery, type DancerYear, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type PopulationEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
4
4
  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";
@@ -15,5 +15,6 @@ 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 { type EntryRow, type StreamQuery, type StreamRow, type StreamScope, entryRank, } from "./stream.js";
18
19
  export { EventFinder, type EventYear, type EventYearQuery, type ResultRoster, type RosterMember, type RosterTeam, TeamFinder, type TeamResult, } from "./teams.js";
19
20
  export { ARCHIVE_TAG, type ArchiveTag, type ReleaseSpec, compareTagsNewestFirst, cutOf, parseArchiveTag, parseReleaseSpec, pickTag, renderArchiveTag, } from "./tag.js";
@@ -1,4 +1,5 @@
1
1
  import type { Release } from "./release.js";
2
+ import { type EntryRow, type StreamQuery, type StreamRow } from "./stream.js";
2
3
  export declare const FEIS_LOCAL_ID_FLOOR = 17179869184;
3
4
  export interface ResultQuery {
4
5
  dancer_id?: number;
@@ -43,6 +44,9 @@ export declare class ResultFinder {
43
44
  private readonly rel;
44
45
  constructor(rel: Release);
45
46
  query(q: ResultQuery): Promise<ResultAnswer>;
47
+ private streamDb;
48
+ stream(q: StreamQuery): AsyncGenerator<StreamRow>;
49
+ entries(q: StreamQuery): AsyncGenerator<EntryRow>;
46
50
  }
47
51
  export interface MajorEvent {
48
52
  event: string;
@@ -78,5 +82,12 @@ export declare class MajorsFinder {
78
82
  tag: string;
79
83
  competitions: MajorCompetition[];
80
84
  }>;
85
+ tabulated(q?: {
86
+ min_true_round?: number;
87
+ }): Promise<{
88
+ tag: string;
89
+ min_true_round: number;
90
+ addresses: string[];
91
+ }>;
81
92
  results(q: ResultQuery): Promise<ResultAnswer>;
82
93
  }
@@ -0,0 +1,73 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export type StreamScope = "majors" | "transcribed" | "platform" | "all";
3
+ export interface StreamQuery {
4
+ scope: StreamScope;
5
+ event?: string | string[];
6
+ year?: number | [number, number];
7
+ tier?: string[];
8
+ category?: string[];
9
+ feis_level?: number[];
10
+ dancer_id?: number[];
11
+ school_id?: string;
12
+ primary?: boolean;
13
+ }
14
+ export interface StreamRow {
15
+ address: string;
16
+ competition: string;
17
+ event: string;
18
+ year: number | null;
19
+ tier: string;
20
+ comp_no: string | null;
21
+ title: string | null;
22
+ category: string | null;
23
+ age: string | null;
24
+ age_printed: string | null;
25
+ gender: string | null;
26
+ competitors: number | null;
27
+ feis_level: number | null;
28
+ level_printed: string | null;
29
+ date_printed: string | null;
30
+ source: string;
31
+ round: string | null;
32
+ feis_id: string | null;
33
+ feis_date_lo: string | null;
34
+ feis_date_hi: string | null;
35
+ section: string;
36
+ card: string;
37
+ place: number | null;
38
+ place_printed: string;
39
+ tie: number;
40
+ relative_place: number | null;
41
+ atts: string[];
42
+ withdrawn: string | null;
43
+ qualifier: string | null;
44
+ name_printed: string;
45
+ name: string | null;
46
+ school_printed: string;
47
+ school_loc: string;
48
+ school_id: string | null;
49
+ dancer_id: number | null;
50
+ dancer_scope: "per-release" | "stable" | null;
51
+ yob: number | null;
52
+ yob_printed: string | null;
53
+ points_printed: string | null;
54
+ platform_dancer_id: string | null;
55
+ primary_source: string | null;
56
+ file: string;
57
+ line: number;
58
+ }
59
+ export interface EntryRow extends StreamRow {
60
+ recall_place: number | null;
61
+ }
62
+ export declare function entryRank(section: string): number;
63
+ export declare const scopeDb: (s: StreamScope) => "majors" | "results";
64
+ export declare function streamSql(q: StreamQuery): {
65
+ sql: string;
66
+ args: (string | number)[];
67
+ };
68
+ export declare function feisDates(db: Database): Map<string, {
69
+ lo: string | null;
70
+ hi: string | null;
71
+ }>;
72
+ export declare function streamRows(db: Database, q: StreamQuery): AsyncGenerator<StreamRow>;
73
+ export declare function entryRows(db: Database, q: StreamQuery): AsyncGenerator<EntryRow>;
@@ -25,6 +25,9 @@ export interface TeamResult {
25
25
  tie: number | null;
26
26
  competitors: number | null;
27
27
  atts: string[];
28
+ age?: string | null;
29
+ gender?: string | null;
30
+ printed_status?: string[];
28
31
  }
29
32
  export interface RosterMember {
30
33
  seq: number;
@@ -33,6 +36,7 @@ export interface RosterMember {
33
36
  yob: number | null;
34
37
  dancer_id: number | null;
35
38
  dancer_scope: "per-release" | "stable" | null;
39
+ gender?: string | null;
36
40
  }
37
41
  export interface RosterTeam {
38
42
  team: string;
@@ -45,6 +49,8 @@ export interface RosterTeam {
45
49
  seq: number | null;
46
50
  school_id: string | null;
47
51
  school_printed: string | null;
52
+ school_printed_as_of?: string | null;
53
+ dance?: string | null;
48
54
  members: RosterMember[];
49
55
  }
50
56
  export interface ResultRoster {
@@ -78,6 +84,7 @@ export declare class TeamFinder {
78
84
  private readonly rel;
79
85
  constructor(rel: Release);
80
86
  private db;
87
+ private hasFields;
81
88
  results(q?: EventYearQuery): Promise<{
82
89
  tag: string;
83
90
  asked: EventYearQuery;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",