@mason-data/cli 0.7.0 → 0.9.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 +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TEXT IN A GLIBC LOCALE'S ORDER, MEASURED FROM THE SOURCE (the BI-tools test of #517: Metabase's field lists
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* TEXT IN A GLIBC LOCALE'S ORDER, MEASURED FROM THE SOURCE (the BI-tools test of #517: Metabase's field lists under
|
|
3
|
+
* en_US.utf8 listed Hangul, `$` / `€` and U+FFFD elsewhere than Postgres; issue 128: one emoji's U+FE0F refused a whole
|
|
4
|
+
* column's sort). The order is never assumed: it is READ from the source database, once (read-only SELECTs over
|
|
5
|
+
* constants, no table, a few seconds), kept in the copy (`_mason.libc_order`, src/duck/store.ts), read again when the
|
|
6
|
+
* database's collation version moves (`pg_database.datcollversion`: glibc's), and every sort key is built from it, in
|
|
7
|
+
* one JavaScript call a row (no real `strcoll` is reachable: not the source's per row, and this machine's glibc, if it
|
|
8
|
+
* has one, is another version of another table).
|
|
9
9
|
*
|
|
10
|
-
* What glibc compares
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
10
|
+
* What glibc's strcoll compares (read from its `iso14651_t1_common` and held to Postgres 18 / glibc 2.41 en_US.utf8 over
|
|
11
|
+
* random strings of 15 scripts): each ELEMENT of the string, a character or a contraction (Thai's vowel written before
|
|
12
|
+
* its consonant, `เก`; an NFD spelling of a letter of its own, `И` + U+0306), has weights at four levels, each a
|
|
13
|
+
* sequence, any of them empty, and the levels are compared in turn, each over the whole string:
|
|
14
|
+
* 1. letters, digits, `$`, U+FFFD; not punctuation, spaces, emoji or marks; a ligature is several (`ß` = ss);
|
|
15
|
+
* 2. accents: a letter's BASE, a combining mark's own, `é` = BASE AIGUT, and `e` + U+0301 is the same sequence.
|
|
16
|
+
* AN ELEMENT OF glibc's <SPECIAL> SECTION (digits, symbols, punctuation, marks) IS `backward` HERE: a run of them is
|
|
17
|
+
* read in reverse, and a run closed by a forward element SKIPS the element before the run's last (get_next_seq's
|
|
18
|
+
* `backw` is decremented twice; that is what `1-X < 1x` is: the `1` is not read, so that side is shorter);
|
|
19
|
+
* 3. case and form (MIN, CAP, COMPAT, FONT …);
|
|
20
|
+
* 4. the last: the pairs (its position, its weight) of each element with a weight there, position first — `三 < :三`
|
|
21
|
+
* (`三` has none) and `가三 < 三가` both follow. What the table does not list (Hangul, an unassigned or a private use
|
|
22
|
+
* code point) has no other level, and is ONE ELEMENT PER BYTE of its UTF-8, each of one shared weight (findidx
|
|
23
|
+
* takes its lead byte alone): U+0378 is two of them, U+E000 three, U+F0000 four.
|
|
24
|
+
* A tie at every level is Postgres's byte order (`varstr_cmp`'s strcmp), which the caller adds.
|
|
25
|
+
*
|
|
26
|
+
* HOW IT IS READ, by comparisons alone (`readLibcOrder`), so any table answers, a tailored locale's or another glibc's:
|
|
27
|
+
* the characters sorted once, each neighbour asked which level told it apart; a weight's place by binary search among
|
|
28
|
+
* the marks or the punctuation; every spelling a character could stand for (its NFD, its NFKD, a Thai or Lao reordering)
|
|
29
|
+
* proposed by Unicode's data and KEPT ONLY WHEN THE SOURCE SAYS THE TWO ARE EQUAL.
|
|
30
|
+
*
|
|
31
|
+
* WHAT IS READ IS HELD, AND THE VERDICT IS A BRANCH (`holdLibcOrder`): pairs of strings, ASCII first, compared by the source and
|
|
32
|
+
* by the key, counted, every pair. A locale of the plain table (`isPlainTable`: en_*, glibc's iso14651_t1 as it comes)
|
|
33
|
+
* IS NEVER REFUSED, of any character, on any C library: what differs stays as it was read, and is recorded
|
|
34
|
+
* (`LibcOrder.held`: the log and mason status), and what no level could be read of is left to the bytes. A difference
|
|
35
|
+
* there is a decided one (`libc-rare-letter`, `libc-mark-run-form`). Any other locale has what differs uncovered where it
|
|
36
|
+
* is rare; a pair that differs in ASCII or Latin alone is a table this reading does not follow (tr_TR, sv_SE, cs_CZ;
|
|
37
|
+
* another C library), and the whole order is `unread`.
|
|
38
|
+
* REFUSED BY NAME under such a locale alone, never an exit at the start and never another order: an `unread` order refuses
|
|
39
|
+
* every text it is asked to order (`libc-locale-unread`), a character not covered the values holding it
|
|
40
|
+
* (`libc-uncovered`), and so does every code point not read where a sample of them does not share one weight. A strcoll
|
|
41
|
+
* that is byte order (musl's, macOS's) reads as that (`bytes`), with nothing else to read.
|
|
30
42
|
*/
|
|
43
|
+
import { Buffer } from "node:buffer";
|
|
31
44
|
import { gapMark } from "./gaps.js";
|
|
32
45
|
import { PgEvalError } from "./pg-eval.js";
|
|
33
46
|
const MAX = 0x110000;
|
|
34
|
-
|
|
47
|
+
const BATCH = 20000;
|
|
48
|
+
/** what a code point not read is: the first of the private use area, which no collation table lists */
|
|
49
|
+
const UNLISTED = 0xe000;
|
|
50
|
+
/** one in this many of the code points not read is held to `UNLISTED` */
|
|
51
|
+
const UNREAD_SAMPLE = 211;
|
|
52
|
+
/** the pairs of random strings of each pool (all, ASCII, Latin) a read order is held to (`holdLibcOrder`) */
|
|
53
|
+
const HELD_PAIRS = 6000;
|
|
54
|
+
/** a ligature's units whose case is searched for: longer ones (Arabic's) are left to `hold` */
|
|
55
|
+
const MAX_UNITS = 4;
|
|
56
|
+
/** a mark class's place at the second level, and the place just below it of a weight no mark has (the `nth` of its letter) */
|
|
57
|
+
const markPlace = (k) => (2 * k + 2) * 4096;
|
|
58
|
+
const gapPlace = (k, nth) => (2 * k + 1) * 4096 + nth;
|
|
59
|
+
/** the third level's weight of a case's rank in its letter (rank 0 is 1, the common one), and of one between two ranks */
|
|
60
|
+
const caseOf = (rank) => 2 * rank * 1024 + 1;
|
|
61
|
+
const caseBelow = (rank, nth) => (2 * rank - 1) * 1024 + nth + 1;
|
|
62
|
+
/** The code points read from the source: assigned, not private use, not a surrogate (`UNREAD_SAMPLE` holds the rest). */
|
|
35
63
|
export function assignedCodePoints() {
|
|
36
64
|
const out = [];
|
|
37
65
|
for (let g = 1; g < MAX; g++) {
|
|
@@ -43,14 +71,165 @@ export function assignedCodePoints() {
|
|
|
43
71
|
}
|
|
44
72
|
return out;
|
|
45
73
|
}
|
|
46
|
-
/** what glibc orders otherwise than this ordering: combining marks, Thai and Lao vowels written before their consonant */
|
|
47
|
-
const UNCOVERED = /[\p{M}เ-ไເ-ໄ]/u;
|
|
48
74
|
const ints = (xs) => `'{${xs.join(",")}}'::int[]`;
|
|
49
75
|
const texts = (xs) => `ARRAY[${xs.map((x) => `'${x.replaceAll("'", "''")}'`).join(",")}]::text[]`;
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
const cps = (s) => [...s].map((c) => c.codePointAt(0));
|
|
77
|
+
const str = (xs) => String.fromCodePoint(...xs);
|
|
78
|
+
/** An empty order: nothing covered. */
|
|
79
|
+
export function emptyLibcOrder(collate, version) {
|
|
80
|
+
return { collate, version, bytes: false, unread: false, held: { pairs: 0, differ: 0, common: 0 }, l1: new Uint32Array(MAX), l2: new Uint32Array(MAX), l3: new Uint32Array(MAX), l4: new Uint32Array(MAX), back: new Uint8Array(MAX),
|
|
81
|
+
uncovered: new Uint8Array(MAX).fill(1), unlisted: new Uint8Array(MAX), special: new Uint8Array(MAX), sequences: new Map(), contractions: new Map() };
|
|
82
|
+
}
|
|
83
|
+
/** the order with `special` said from its sequences and contractions */
|
|
84
|
+
export function withLookups(o) {
|
|
85
|
+
o.special.set(o.unlisted);
|
|
86
|
+
for (const g of o.sequences.keys())
|
|
87
|
+
o.special[g] = 1;
|
|
88
|
+
for (const g of o.contractions.keys())
|
|
89
|
+
o.special[g] = 1;
|
|
90
|
+
return o;
|
|
91
|
+
}
|
|
92
|
+
/** the element of one character, its sequence or its one weight at each level */
|
|
93
|
+
function elementOf(o, g) {
|
|
94
|
+
const s = o.sequences.get(g);
|
|
95
|
+
if (s !== undefined)
|
|
96
|
+
return s;
|
|
97
|
+
const one = (x) => x > 0 ? [x] : [];
|
|
98
|
+
return { l1: one(o.l1[g]), l2: one(o.l2[g]), l3: one(o.l3[g]), l4: one(o.l4[g]), back: o.back[g] === 1 };
|
|
99
|
+
}
|
|
100
|
+
const joined = (es) => ({ l1: es.flatMap((e) => e.l1), l2: es.flatMap((e) => e.l2), l3: es.flatMap((e) => e.l3), l4: es.flatMap((e) => e.l4), back: es[0].back });
|
|
101
|
+
/** Asks the source about pairs of code points (`a`, `b` in `expr`), in batches: its answer to each. */
|
|
102
|
+
async function asked(query, pairs, expr) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (let at = 0; at < pairs.length; at += BATCH) {
|
|
105
|
+
const part = pairs.slice(at, at + BATCH);
|
|
106
|
+
const rows = await query(`SELECT (${expr})::text FROM unnest(${ints(part.map((p) => p[0]))}, ${ints(part.map((p) => p[1]))}) WITH ORDINALITY AS u(a, b, n) ORDER BY n`);
|
|
107
|
+
if (rows.length !== part.length)
|
|
108
|
+
throw new Error(`reading how the database orders text: ${part.length} comparisons asked, ${rows.length} answered`);
|
|
109
|
+
for (const [v] of rows)
|
|
110
|
+
out.push(v === "true");
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** Asks the source about pairs of strings (`s`, `t` in `expr`), in batches: its answer to each. */
|
|
115
|
+
async function askedText(query, pairs, expr) {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (let at = 0; at < pairs.length; at += BATCH / 4) {
|
|
118
|
+
const part = pairs.slice(at, at + BATCH / 4);
|
|
119
|
+
const rows = await query(`SELECT (${expr})::text FROM unnest(${texts(part.map((p) => p[0]))}, ${texts(part.map((p) => p[1]))}) WITH ORDINALITY AS u(s, t, n) ORDER BY n`);
|
|
120
|
+
if (rows.length !== part.length)
|
|
121
|
+
throw new Error(`reading how the database orders text: ${part.length} comparisons asked, ${rows.length} answered`);
|
|
122
|
+
for (const [v] of rows)
|
|
123
|
+
out.push(v === "true");
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** a binary search of many at once: `below(i, mid)` asked of each still open, until each has its lowest mid answering yes */
|
|
128
|
+
async function searched(n, top, ask) {
|
|
129
|
+
const lo = new Int32Array(n), hi = new Int32Array(n);
|
|
130
|
+
for (let i = 0; i < n; i++)
|
|
131
|
+
hi[i] = top(i);
|
|
132
|
+
for (;;) {
|
|
133
|
+
const open = [];
|
|
134
|
+
for (let i = 0; i < n; i++)
|
|
135
|
+
if (lo[i] < hi[i])
|
|
136
|
+
open.push({ i, mid: (lo[i] + hi[i]) >> 1 });
|
|
137
|
+
if (open.length === 0)
|
|
138
|
+
return lo;
|
|
139
|
+
const yes = await ask(open);
|
|
140
|
+
yes.forEach((y, j) => { const { i, mid } = open[j]; if (y)
|
|
141
|
+
hi[i] = mid;
|
|
142
|
+
else
|
|
143
|
+
lo[i] = mid + 1; });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A locale that is glibc's table as it comes, so one this reading is held to by design and never refuses: English's,
|
|
148
|
+
* each `en_XX` being `copy "iso14651_t1"` (en_CA moves capitals before small letters, which the read asks of the source;
|
|
149
|
+
* en_AG, en_BW, en_IL and en_ZW copy another of these). Nothing else is, de_DE and fr_FR included, though most hold: a
|
|
150
|
+
* tailoring is not told from a name, so those are held to the measurement alone.
|
|
151
|
+
*/
|
|
152
|
+
export const isPlainTable = (collate) => /^en_[A-Za-z]{2}([.@]|$)/.test(collate);
|
|
153
|
+
/** code points in byte order, of each kind a collation moves: what a byte-ordering strcoll keeps in this order */
|
|
154
|
+
const BYTE_ORDER_SAMPLE = [0x20, 0x21, 0x2d, 0x30, 0x41, 0x5a, 0x5f, 0x61, 0x7a, 0xc0, 0xe9, 0x100, 0x300, 0x4e00, 0xac00, 0xe000, 0xfffd, 0x1f600];
|
|
155
|
+
/**
|
|
156
|
+
* A symbol's first level, its letters `head` and the class its last weight lies above: `lo` is the first of `classes`
|
|
157
|
+
* it sorts below, so none (0) leaves the letters alone and all of them (`classes.length`) means the last.
|
|
158
|
+
*/
|
|
159
|
+
export const closedWeights = (head, classes, lo) => lo === 0 ? [...head] : [...head, classes[lo - 1]];
|
|
160
|
+
/**
|
|
161
|
+
* A READ UNDER A NAMED COLLATION (`collation`, its SQL name: `pg_catalog."fr_FR"`): every text the read compares or
|
|
162
|
+
* sorts is made of `chr(…)` or of an array of strings, so each of those carries `COLLATE <collation>` (explicit, it
|
|
163
|
+
* decides every comparison and ORDER BY it is in), outside the read's string literals. A read of the database's own
|
|
164
|
+
* collation sends its SQL as it is.
|
|
165
|
+
*/
|
|
166
|
+
export function collatedSql(sql, collation) {
|
|
167
|
+
let out = "";
|
|
168
|
+
for (let i = 0; i < sql.length;) {
|
|
169
|
+
if (sql[i] === "'") {
|
|
170
|
+
let j = i + 1;
|
|
171
|
+
for (; j < sql.length; j++) {
|
|
172
|
+
if (sql[j] === "'") {
|
|
173
|
+
if (sql[j + 1] === "'") {
|
|
174
|
+
j++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
out += sql.slice(i, j + 1);
|
|
181
|
+
i = j + 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (sql.startsWith("chr(", i) && !/[\w.]/.test(sql[i - 1] ?? "")) {
|
|
185
|
+
let depth = 0, j = i + 3;
|
|
186
|
+
for (; j < sql.length; j++) {
|
|
187
|
+
if (sql[j] === "(")
|
|
188
|
+
depth++;
|
|
189
|
+
else if (sql[j] === ")" && --depth === 0)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// (its argument is a code point's number: no text of its own)
|
|
193
|
+
out += `(${sql.slice(i, j + 1)} COLLATE ${collation})`;
|
|
194
|
+
i = j + 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (sql.startsWith("]::text[]", i)) {
|
|
198
|
+
out += `]::text[] COLLATE ${collation}`;
|
|
199
|
+
i += 9;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
out += sql[i];
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/** The source's order of text under its default collation, or under the named `collation` (`collatedSql`), as the
|
|
208
|
+
* module's header says: `collate` is the locale (collcollate) it is read and kept under. */
|
|
209
|
+
export async function readLibcOrder(source, collate, version, collation) {
|
|
210
|
+
const query = collation === undefined ? source : (sql) => source(collatedSql(sql, collation));
|
|
211
|
+
const o = emptyLibcOrder(collate, version);
|
|
212
|
+
const held = () => holdLibcOrder(o, async (pairs) => {
|
|
213
|
+
const less = await askedText(query, pairs, "s < t");
|
|
214
|
+
const more = await askedText(query, pairs, "s > t");
|
|
215
|
+
return pairs.map((_, i) => less[i] ? -1 : more[i] ? 1 : 0);
|
|
216
|
+
});
|
|
217
|
+
// a strcoll with no level glibc's has, another C library's: not followed, but for the plain table's name, which is
|
|
218
|
+
// never refused: the first level where that is all there is, the bytes for the rest, and what that differs on recorded
|
|
219
|
+
const noLevels = async () => {
|
|
220
|
+
if (!isPlainTable(collate)) {
|
|
221
|
+
o.unread = true;
|
|
222
|
+
return o;
|
|
223
|
+
}
|
|
224
|
+
o.uncovered.fill(0);
|
|
225
|
+
await held();
|
|
226
|
+
return o;
|
|
227
|
+
};
|
|
228
|
+
const inOrder = await asked(query, BYTE_ORDER_SAMPLE.slice(1).map((g, i) => [BYTE_ORDER_SAMPLE[i], g]), "chr(a) < chr(b)");
|
|
229
|
+
if (inOrder.every(Boolean))
|
|
230
|
+
return { ...o, bytes: true };
|
|
52
231
|
const points = assignedCodePoints();
|
|
53
|
-
|
|
232
|
+
points.push(UNLISTED);
|
|
54
233
|
// each character: whether it has a first-level weight ('b'||c < 'ba' and c||'b' > 'aa' only for one that has none);
|
|
55
234
|
// those that have one in the database's order, each marked when its neighbour below is of its class; the rest in
|
|
56
235
|
// the database's order, each marked when its neighbour below has the same last-level weight (pp > c only then)
|
|
@@ -63,27 +242,135 @@ export async function readLibcOrder(query, collate, version) {
|
|
|
63
242
|
// each kind in the database's order
|
|
64
243
|
rows.sort((a, b) => a[0] === b[0] ? Number(a[3]) - Number(b[3]) : a[0] < b[0] ? -1 : 1);
|
|
65
244
|
const weighted = [];
|
|
66
|
-
const
|
|
67
|
-
|
|
245
|
+
const classes = []; // the characters of each first-level class, in the database's order
|
|
246
|
+
const noFirst = [];
|
|
68
247
|
for (const [kind, g0, same] of rows) {
|
|
69
248
|
const g = Number(g0);
|
|
70
|
-
uncovered[g] = UNCOVERED.test(String.fromCodePoint(g)) ? 1 : 0;
|
|
71
249
|
if (kind === "n") {
|
|
72
|
-
if (same !== "true")
|
|
73
|
-
|
|
74
|
-
|
|
250
|
+
if (same !== "true" || classes.length === 0)
|
|
251
|
+
classes.push([]);
|
|
252
|
+
classes.at(-1).push(g);
|
|
75
253
|
weighted.push(g);
|
|
254
|
+
o.l1[g] = classes.length;
|
|
76
255
|
}
|
|
77
|
-
else
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
256
|
+
else
|
|
257
|
+
noFirst.push({ g, same: same === "true" });
|
|
258
|
+
}
|
|
259
|
+
if (weighted.length === 0 || noFirst.length === 0)
|
|
260
|
+
return noLevels();
|
|
261
|
+
for (const g of points)
|
|
262
|
+
o.uncovered[g] = 0;
|
|
263
|
+
/** a character's place among those the last level compares it with: its first-level class's, or all the marks */
|
|
264
|
+
const ties = new Map();
|
|
265
|
+
const nfd = (g) => cps(String.fromCodePoint(g).normalize("NFD"));
|
|
266
|
+
// of the characters with no first-level weight, those with a second-level one: a mark's, above BASE, outranks the case
|
|
267
|
+
// of a later letter. The others sort below them: each last-level class one character, in order
|
|
268
|
+
const isMark = await asked(query, noFirst.map((c) => [c.g, c.g]), "'x' || chr(a) || 'x' > 'xX'");
|
|
269
|
+
const ignoredClasses = [];
|
|
270
|
+
const marksRead = [];
|
|
271
|
+
noFirst.forEach(({ g, same }, i) => {
|
|
272
|
+
if (isMark[i]) {
|
|
273
|
+
marksRead.push(g);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!same || ignoredClasses.length === 0)
|
|
277
|
+
ignoredClasses.push(g);
|
|
278
|
+
o.l4[g] = 2 * ignoredClasses.length;
|
|
279
|
+
});
|
|
280
|
+
if (ignoredClasses.length === 0)
|
|
281
|
+
return noLevels();
|
|
282
|
+
// the marks at the second level: in order behind a letter, each class of equal ones, and their case within
|
|
283
|
+
const marks = marksRead.length === 0 ? [] : (await query(`SELECT g::text FROM unnest(${ints(marksRead)}) AS u(g) ORDER BY 'x' || chr(g)`)).map(([g]) => Number(g));
|
|
284
|
+
const pairsOf = (xs) => xs.slice(1).map((x, i) => [xs[i], x]);
|
|
285
|
+
const mark = marks[0];
|
|
286
|
+
/** the two are equal at the first level: and at the second? (only a later mark told them apart) */
|
|
287
|
+
const sameSecond = (x, y) => `${x} || 'x' || chr(${mark}) > ${y} || 'x'`;
|
|
288
|
+
const sameCase = (x, y) => `${x} || 'X' > ${y} || 'x'`;
|
|
289
|
+
const markClasses = []; // the marks of each second-level class, in order
|
|
290
|
+
if (mark !== undefined) {
|
|
291
|
+
const adjacent = pairsOf(marks);
|
|
292
|
+
const [second, third] = [await asked(query, adjacent, sameSecond("'x' || chr(a)", "'x' || chr(b)")), await asked(query, adjacent, sameCase("'x' || chr(a)", "'x' || chr(b)"))];
|
|
293
|
+
let rank = 0;
|
|
294
|
+
marks.forEach((g, i) => {
|
|
295
|
+
if (i === 0 || !second[i - 1]) {
|
|
296
|
+
markClasses.push([]);
|
|
297
|
+
rank = 0;
|
|
298
|
+
}
|
|
299
|
+
else if (!third[i - 1])
|
|
300
|
+
rank++;
|
|
301
|
+
markClasses.at(-1).push(g);
|
|
302
|
+
o.l2[g] = markPlace(markClasses.length - 1);
|
|
303
|
+
o.l3[g] = caseOf(rank);
|
|
304
|
+
// (a skipped mark, the module's header, is compared at the last level with a mark of another class: all in one order)
|
|
305
|
+
ties.set(g, i);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// each class of the first level: which level tells its neighbours apart
|
|
309
|
+
const groupOf = new Map();
|
|
310
|
+
{
|
|
311
|
+
const adjacent = classes.flatMap(pairsOf);
|
|
312
|
+
const sameSeconds = mark === undefined ? adjacent.map(() => true) : await asked(query, adjacent, sameSecond("chr(a)", "chr(b)"));
|
|
313
|
+
const sameThirds = await asked(query, adjacent, sameCase("chr(a)", "chr(b)"));
|
|
314
|
+
let at = 0;
|
|
315
|
+
for (const cls of classes) {
|
|
316
|
+
let second = 0, third = 0;
|
|
317
|
+
cls.forEach((g, i) => {
|
|
318
|
+
if (i > 0) {
|
|
319
|
+
if (!sameSeconds[at]) {
|
|
320
|
+
second++;
|
|
321
|
+
third = 0;
|
|
322
|
+
}
|
|
323
|
+
else if (!sameThirds[at])
|
|
324
|
+
third++;
|
|
325
|
+
at++;
|
|
326
|
+
}
|
|
327
|
+
groupOf.set(g, { second, third });
|
|
328
|
+
// (`é` and `e` + U+0301 are equal down to here: the last level compares a letter with its class's others)
|
|
329
|
+
ties.set(g, i);
|
|
330
|
+
});
|
|
81
331
|
}
|
|
82
332
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
333
|
+
// which elements are read backward at the second level, and which have no second level at all (CJK's, here): a
|
|
334
|
+
// character x then a mark m is [BASE, m], m then x [m, BASE], and they turn round when x is backward; closed by a letter
|
|
335
|
+
// the run of the backward one skips the element before its last (the module's header), which tells it from one with none
|
|
336
|
+
const noSecond = new Set();
|
|
337
|
+
if (mark !== undefined) {
|
|
338
|
+
const each = weighted.map((g) => [g, mark]);
|
|
339
|
+
const forward = await asked(query, each, "chr(a) || chr(b) < chr(b) || chr(a)");
|
|
340
|
+
const backward = await asked(query, each, "chr(b) || chr(a) || 'X' < chr(a) || chr(b) || 'x'");
|
|
341
|
+
weighted.forEach((g, i) => { if (!forward[i]) {
|
|
342
|
+
if (backward[i])
|
|
343
|
+
o.back[g] = 1;
|
|
344
|
+
else
|
|
345
|
+
noSecond.add(g);
|
|
346
|
+
} });
|
|
347
|
+
const [low, high] = [markClasses[0]?.[0], markClasses[1]?.[0]];
|
|
348
|
+
if (low !== undefined && high !== undefined) {
|
|
349
|
+
const rest = noFirst.map((c) => c.g);
|
|
350
|
+
const turned = await asked(query, rest.map((g) => [g, g]), `'x' || chr(${low}) || chr(a) || chr(${high}) || 'x' > 'x' || chr(${high}) || chr(a) || chr(${low}) || 'x'`);
|
|
351
|
+
rest.forEach((g, i) => { if (turned[i])
|
|
352
|
+
o.back[g] = 1; });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// a mark of two (U+0344): those two
|
|
356
|
+
for (const g of marks) {
|
|
357
|
+
const parts = nfd(g);
|
|
358
|
+
if (parts.length > 1 && parts.every((p) => o.l2[p] > 0))
|
|
359
|
+
o.sequences.set(g, { ...joined(parts.map((p) => elementOf(o, p))), back: o.back[g] === 1 });
|
|
360
|
+
}
|
|
361
|
+
// the plain ones' second and third levels: the first of a class's accents is BASE, each case its rank
|
|
362
|
+
for (const g of weighted) {
|
|
363
|
+
if (noSecond.has(g))
|
|
364
|
+
continue;
|
|
365
|
+
const { second, third } = groupOf.get(g);
|
|
366
|
+
o.l2[g] = 1;
|
|
367
|
+
o.l3[g] = second === 0 ? caseOf(third) : 1;
|
|
368
|
+
}
|
|
369
|
+
/** whether each pair of strings is equal at the second level, and at the first three */
|
|
370
|
+
const equalSecond = (ps) => mark === undefined ? Promise.resolve(ps.map(() => true)) : askedText(query, ps, `(${sameSecond("s", "t")}) AND (${sameSecond("t", "s")})`);
|
|
371
|
+
const equalAt = (ps) => askedText(query, ps, `(s || 'y' > t || 'x') AND (t || 'y' > s || 'x') ${mark === undefined ? "" : `AND (${sameSecond("s", "t")}) AND (${sameSecond("t", "s")})`} AND (${sameCase("s", "t")}) AND (${sameCase("t", "s")})`);
|
|
372
|
+
// a ligature's first level: proposed by its compatibility decomposition (its marks dropped) and the letters NFKD keeps
|
|
373
|
+
// whole, kept where the source says the two are equal at the first level
|
|
87
374
|
const proposed = [];
|
|
88
375
|
for (const g of weighted) {
|
|
89
376
|
const d = String.fromCodePoint(g).normalize("NFKD").replace(/\p{M}/gu, "");
|
|
@@ -93,98 +380,577 @@ export async function readLibcOrder(query, collate, version) {
|
|
|
93
380
|
for (const [ch, e] of [["ß", "ss"], ["ẞ", "SS"], ["æ", "ae"], ["Æ", "AE"], ["œ", "oe"], ["Œ", "OE"], ["þ", "th"], ["Þ", "TH"]])
|
|
94
381
|
proposed.push([ch.codePointAt(0), e]);
|
|
95
382
|
const expansions = new Map();
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expansions.set(
|
|
101
|
-
}
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
383
|
+
{
|
|
384
|
+
const eq = await askedText(query, proposed.map(([g, e]) => [String.fromCodePoint(g), e]), "(s || 'b' > t || 'a') AND (t || 'b' > s || 'a')");
|
|
385
|
+
// (of the units with a first-level weight: `⅖` is 2, 5, the slash between them weighs nothing there)
|
|
386
|
+
proposed.forEach(([g, e], i) => { const units = cps(e).filter((u) => o.l1[u] > 0); if (eq[i] && units.length > 1)
|
|
387
|
+
expansions.set(g, units); });
|
|
388
|
+
}
|
|
389
|
+
// a ligature Unicode does not decompose (ȸ, Ꜷ): sorted among its first letter's words, so below that letter and the
|
|
390
|
+
// highest character; each unit found by binary search among the single letters, kept when the source says equal
|
|
391
|
+
{
|
|
392
|
+
const whole = classes.map((c) => c[0]);
|
|
393
|
+
const highest = whole.at(-1);
|
|
394
|
+
const single = new Set(whole.filter((g) => !expansions.has(g)));
|
|
395
|
+
for (let round = 0; round < 4; round++) {
|
|
396
|
+
const asking = [];
|
|
397
|
+
let below = whole[0];
|
|
398
|
+
for (const g of whole) {
|
|
399
|
+
if (single.has(g) && g !== below)
|
|
400
|
+
asking.push([g, below]);
|
|
401
|
+
if (single.has(g))
|
|
402
|
+
below = g;
|
|
403
|
+
}
|
|
404
|
+
const yes = await asked(query, asking, `chr(a) || 'x' < chr(b) || chr(${highest})`);
|
|
405
|
+
const found = asking.filter((_, i) => yes[i]);
|
|
406
|
+
if (found.length === 0)
|
|
407
|
+
break;
|
|
408
|
+
for (const [g] of found)
|
|
409
|
+
single.delete(g);
|
|
410
|
+
const letters = whole.filter((g) => single.has(g));
|
|
411
|
+
const lo = await searched(found.length, () => letters.length - 1, (open) => askedText(query, open.map(({ i, mid }) => [String.fromCodePoint(found[i][0]), str([found[i][1], letters[mid], highest])]), "s < t"));
|
|
412
|
+
const spelled = found.map(([g, first], i) => [g, str([first, letters[lo[i]]])]);
|
|
413
|
+
const eq = await askedText(query, spelled.map(([g, e]) => [String.fromCodePoint(g), e]), "(s || 'b' > t || 'a') AND (t || 'b' > s || 'a')");
|
|
414
|
+
// (every character of its class is that spelling)
|
|
415
|
+
spelled.forEach(([g, e], i) => { if (eq[i])
|
|
416
|
+
for (const c of classes[o.l1[g] - 1])
|
|
417
|
+
if (!expansions.has(c))
|
|
418
|
+
expansions.set(c, cps(e)); });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// a ligature of several letters whose spelling the source did not confirm has first-level weights no letter has
|
|
422
|
+
// (`㏩`: 10 and a CJK weight of its own): nothing here can say them. Under the plain table it stays, at the one weight
|
|
423
|
+
// it sorted by, a decided difference (`libc-rare-letter`)
|
|
424
|
+
for (const [g, e] of proposed)
|
|
425
|
+
if (!expansions.has(g) && e === String.fromCodePoint(g).normalize("NFKD").replace(/\p{M}/gu, "") && cps(e).filter((u) => o.l1[u] > 0).length > 1 && !isPlainTable(collate))
|
|
426
|
+
o.uncovered[g] = 1;
|
|
427
|
+
// an accented letter: its NFD's parts, once they are read
|
|
428
|
+
const decomposed = new Set(weighted.filter((g) => !expansions.has(g) && nfd(g).length > 1));
|
|
429
|
+
/** a weight of its own, its place among the marks': after `before`, before `after` (`nth` its letter's accent) */
|
|
430
|
+
const place = async (items) => {
|
|
431
|
+
const lo = await searched(items.length, () => markClasses.length, (open) => askedText(query, open.map(({ i, mid }) => [String.fromCodePoint(items[i].g), `${items[i].before}${String.fromCodePoint(markClasses[mid][0])}${items[i].after}`]), "s || 'x' < t || 'x'"));
|
|
432
|
+
const same = await equalSecond(items.map((it, i) => [String.fromCodePoint(it.g), `${it.before}${String.fromCodePoint(markClasses[Math.max(lo[i] - 1, 0)][0])}${it.after}`]));
|
|
433
|
+
return items.map((it, i) => lo[i] > 0 && same[i] ? markPlace(lo[i] - 1) : gapPlace(lo[i], it.nth));
|
|
434
|
+
};
|
|
435
|
+
// a letter of accents its NFD does not spell (ø: BASE and a weight of its own)
|
|
436
|
+
if (markClasses.length > 0) {
|
|
437
|
+
const variants = weighted.filter((g) => !expansions.has(g) && !decomposed.has(g) && !noSecond.has(g) && groupOf.get(g).second > 0);
|
|
438
|
+
const placed = await place(variants.map((g) => ({ g, before: String.fromCodePoint(classes[o.l1[g] - 1][0]), after: "", nth: groupOf.get(g).second })));
|
|
439
|
+
variants.forEach((g, i) => o.sequences.set(g, { l1: [o.l1[g]], l2: [1, placed[i]], l3: [caseOf(groupOf.get(g).third), 1], l4: [], back: o.back[g] === 1 }));
|
|
440
|
+
}
|
|
441
|
+
// a ligature's second level, its letters', or with a weight of its own after the first (ß, æ); its case unit by unit,
|
|
442
|
+
// each searched for among the cases of its letter, the units after it at the highest of theirs
|
|
443
|
+
{
|
|
444
|
+
const list = [...expansions.keys()];
|
|
445
|
+
const units = (g) => expansions.get(g);
|
|
446
|
+
const plain = await equalSecond(list.map((g) => [String.fromCodePoint(g), str(units(g))]));
|
|
447
|
+
const ladder = (u) => {
|
|
448
|
+
const out = [];
|
|
449
|
+
for (const g of classes[o.l1[u] - 1] ?? []) {
|
|
450
|
+
const p = groupOf.get(g);
|
|
451
|
+
if (p.second === 0 && p.third === out.length)
|
|
452
|
+
out.push(g);
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
};
|
|
456
|
+
const flat = list.filter((g, i) => plain[i] && units(g).length <= MAX_UNITS);
|
|
457
|
+
const chosen = new Map(flat.map((g) => [g, []]));
|
|
458
|
+
for (let unit = 0; unit < MAX_UNITS; unit++) {
|
|
459
|
+
const here = flat.filter((g) => units(g).length > unit);
|
|
460
|
+
const lo = await searched(here.length, (i) => ladder(units(here[i])[unit]).length, (open) => askedText(query, open.map(({ i, mid }) => {
|
|
461
|
+
const g = here[i], us = units(g);
|
|
462
|
+
const done = chosen.get(g).map((r, k) => ladder(us[k])[r] ?? ladder(us[k]).at(-1) ?? us[k]);
|
|
463
|
+
const top = us.slice(unit + 1).map((u) => ladder(u).at(-1) ?? u);
|
|
464
|
+
return [String.fromCodePoint(g), str([...done, ladder(us[unit])[mid] ?? us[unit], ...top])];
|
|
465
|
+
}), "s || 'x' < t || 'X'"));
|
|
466
|
+
here.forEach((g, i) => chosen.get(g).push(lo[i]));
|
|
467
|
+
}
|
|
468
|
+
// a case no letter has (a fraction's, a circled digit's) lies between two ranks: those of one spelling in their own order
|
|
469
|
+
const exact = await equalAt(flat.map((g) => [String.fromCodePoint(g), str(chosen.get(g).map((r, k) => ladder(units(g)[k])[r] ?? ladder(units(g)[k]).at(-1) ?? units(g)[k]))]));
|
|
470
|
+
const between = flat.filter((_, i) => !exact[i]);
|
|
471
|
+
const nthOf = new Map();
|
|
472
|
+
if (between.length > 0) {
|
|
473
|
+
const spelledAs = (g) => `${units(g).join(",")}/${chosen.get(g).join(",")}`;
|
|
474
|
+
const sorted = (await query(`SELECT g::text FROM unnest(${ints(between)}) AS u(g) ORDER BY chr(g)`)).map(([g]) => Number(g));
|
|
475
|
+
const same = await asked(query, pairsOf(sorted), sameCase("chr(a)", "chr(b)"));
|
|
476
|
+
let nth = 0;
|
|
477
|
+
sorted.forEach((g, i) => { if (i > 0 && spelledAs(sorted[i - 1]) === spelledAs(g) && !same[i - 1])
|
|
478
|
+
nth++;
|
|
479
|
+
else if (i > 0 && spelledAs(sorted[i - 1]) !== spelledAs(g))
|
|
480
|
+
nth = 0; nthOf.set(g, nth); });
|
|
481
|
+
}
|
|
482
|
+
const own = list.filter((_, i) => !plain[i]);
|
|
483
|
+
const placed = new Map(markClasses.length === 0 ? [] : (await place(own.map((g) => ({ g, before: String.fromCodePoint(units(g)[0]), after: str(units(g).slice(1)), nth: groupOf.get(g).second + 1 })))).map((p, i) => [own[i], p]));
|
|
484
|
+
list.forEach((g, i) => {
|
|
485
|
+
const es = units(g).map((u) => elementOf(o, u));
|
|
486
|
+
const whole = joined(es);
|
|
487
|
+
if (plain[i]) {
|
|
488
|
+
const cases = chosen.get(g), nth = nthOf.get(g);
|
|
489
|
+
if (cases !== undefined)
|
|
490
|
+
whole.l3 = es.flatMap((e, k) => e.l3.length === 0 ? [] : [nth === undefined || cases[k] === 0 ? caseOf(cases[k]) : caseBelow(cases[k], nth), ...e.l3.slice(1)]);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const at = es[0].l2.length;
|
|
494
|
+
whole.l2 = [...whole.l2.slice(0, at), placed.get(g) ?? 1, ...whole.l2.slice(at)];
|
|
495
|
+
whole.l3 = [caseOf(groupOf.get(g).third), ...whole.l3.map(() => 1)];
|
|
110
496
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
497
|
+
o.sequences.set(g, { ...whole, back: o.back[g] === 1 });
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// an accented letter equal to its spelling is that spelling: the letter of one mark fewer and its last mark, never
|
|
501
|
+
// its NFD whole, whose run of marks is read backward and skipped (the module's header). So the fewest marks first
|
|
502
|
+
{
|
|
503
|
+
const spelling = (g) => {
|
|
504
|
+
const d = nfd(g), head = cps(str(d.slice(0, -1)).normalize("NFC"));
|
|
505
|
+
return head.length === 1 ? [head[0], d.at(-1)] : d;
|
|
506
|
+
};
|
|
507
|
+
const ordered = [...decomposed].sort((a, b) => nfd(a).length - nfd(b).length);
|
|
508
|
+
for (let size = 2; ordered.some((g) => nfd(g).length === size); size++) {
|
|
509
|
+
const list = ordered.filter((g) => nfd(g).length === size);
|
|
510
|
+
const spelled = await equalAt(list.map((g) => [String.fromCodePoint(g), str(spelling(g))]));
|
|
511
|
+
list.forEach((g, i) => {
|
|
512
|
+
const parts = joined(spelling(g).map((p) => elementOf(o, p)));
|
|
513
|
+
// (equal to a letter of its own, it is that letter: its spelling is a contraction, below)
|
|
514
|
+
if (spelled[i] && (parts.l1.length > 1 || parts.l1[0] === o.l1[g]))
|
|
515
|
+
o.sequences.set(g, { ...parts, back: o.back[g] === 1 });
|
|
516
|
+
else if (groupOf.get(g).second > 0 && !noSecond.has(g))
|
|
517
|
+
o.sequences.set(g, { l1: [o.l1[g]], l2: [1, gapPlace(markClasses.length, groupOf.get(g).second)], l3: [caseOf(groupOf.get(g).third), 1], l4: [], back: o.back[g] === 1 });
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// a symbol the first spelling did not confirm, under the plain table, which refuses nothing: `㏩` is 1, 0 and a weight no
|
|
522
|
+
// character has, so its letters and the class that weight lies above, found by binary search after them. Close: it
|
|
523
|
+
// sorts in its place among what a column holds (`libc-rare-letter`, the rest)
|
|
524
|
+
if (isPlainTable(collate)) {
|
|
525
|
+
const whole = classes.map((c) => c[0]);
|
|
526
|
+
const stuck = proposed.flatMap(([g, e]) => {
|
|
527
|
+
if (expansions.has(g) || o.sequences.has(g) || e !== String.fromCodePoint(g).normalize("NFKD").replace(/\p{M}/gu, ""))
|
|
528
|
+
return [];
|
|
529
|
+
const units = cps(e).filter((u) => o.l1[u] > 0);
|
|
530
|
+
// (a symbol with no Han or Kana unit: findIndex's -1 is `slice(0, -1)`, and its last unit is the searched class)
|
|
531
|
+
const head = units.slice(0, units.findIndex((u) => /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}/u.test(String.fromCodePoint(u))));
|
|
532
|
+
return head.length > 0 && head.length < units.length ? [{ g, head }] : [];
|
|
533
|
+
});
|
|
534
|
+
const lo = await searched(stuck.length, () => whole.length, (open) => askedText(query, open.map(({ i, mid }) => [String.fromCodePoint(stuck[i].g), str([...stuck[i].head, whole[mid]])]), "s < t"));
|
|
535
|
+
stuck.forEach(({ g, head }, i) => {
|
|
536
|
+
const l1 = closedWeights(head.map((u) => o.l1[u]), whole.map((c) => o.l1[c]), lo[i]);
|
|
537
|
+
o.sequences.set(g, { l1, l2: l1.map(() => 1), l3: l1.map(() => 1), l4: [], back: o.back[g] === 1 });
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
// the last level: the marks' and the letters' weights, each by binary search among the punctuation's (p||x < x exactly
|
|
541
|
+
// when p's weight is below x's), two of one class and case told apart by their order; one placed below all of them has
|
|
542
|
+
// none when x||chr(1) is not below chr(1)||x
|
|
543
|
+
{
|
|
544
|
+
const placing = [...marks, ...weighted];
|
|
545
|
+
const lo = await searched(placing.length, () => ignoredClasses.length, (open) => asked(query, open.map(({ i, mid }) => [ignoredClasses[mid], placing[i]]), "NOT (chr(a) || chr(b) < chr(b))"));
|
|
546
|
+
const bottom = placing.filter((_, i) => lo[i] === 0);
|
|
547
|
+
const yes = await asked(query, bottom.map((x) => [x, x]), "chr(a) || chr(1) < chr(1) || chr(a)");
|
|
548
|
+
const has = new Set(bottom.filter((_, i) => yes[i]));
|
|
549
|
+
// the weights in one order: the punctuation's even, a letter's the odd one above its place, then its tie
|
|
550
|
+
const SCALE = 1 << 13;
|
|
551
|
+
const keyed = [];
|
|
552
|
+
for (const { g } of noFirst)
|
|
553
|
+
if (o.l4[g] > 0)
|
|
554
|
+
keyed.push([g, o.l4[g] * SCALE]);
|
|
555
|
+
placing.forEach((g, i) => { if (lo[i] > 0 || has.has(g))
|
|
556
|
+
keyed.push([g, (2 * lo[i] + 1) * SCALE + Math.min(ties.get(g) ?? 0, SCALE - 1)]); });
|
|
557
|
+
const dense = new Map([...new Set(keyed.map((k) => k[1]))].sort((a, b) => a - b).map((v, i) => [v, i + 1]));
|
|
558
|
+
for (const g of placing)
|
|
559
|
+
o.l4[g] = 0;
|
|
560
|
+
for (const [g, v] of keyed)
|
|
561
|
+
o.l4[g] = dense.get(v);
|
|
562
|
+
for (const [g, e] of o.sequences)
|
|
563
|
+
e.l4 = o.l4[g] > 0 ? [o.l4[g]] : [];
|
|
564
|
+
}
|
|
565
|
+
// the second level's places made its weights, BASE 1
|
|
566
|
+
{
|
|
567
|
+
const values = new Set([1]);
|
|
568
|
+
for (let g = 0; g < MAX; g++)
|
|
569
|
+
if (o.l2[g] > 0)
|
|
570
|
+
values.add(o.l2[g]);
|
|
571
|
+
for (const e of o.sequences.values())
|
|
572
|
+
for (const v of e.l2)
|
|
573
|
+
values.add(v);
|
|
574
|
+
const dense = new Map([...values].sort((a, b) => a - b).map((v, i) => [v, i + 1]));
|
|
575
|
+
for (let g = 0; g < MAX; g++)
|
|
576
|
+
if (o.l2[g] > 0)
|
|
577
|
+
o.l2[g] = dense.get(o.l2[g]);
|
|
578
|
+
for (const e of o.sequences.values())
|
|
579
|
+
e.l2 = e.l2.map((v) => dense.get(v));
|
|
580
|
+
}
|
|
581
|
+
// contractions: a spelling the source says equals what it spells, where its characters read one by one do not
|
|
582
|
+
{
|
|
583
|
+
const proposals = [];
|
|
584
|
+
const reordered = (vowels, consonants) => {
|
|
585
|
+
for (let v = vowels[0]; v <= vowels[1]; v++)
|
|
586
|
+
for (let c = consonants[0]; c <= consonants[1]; c++)
|
|
587
|
+
proposals.push([[v, c], [c, v]]);
|
|
588
|
+
};
|
|
589
|
+
reordered([0x0e40, 0x0e44], [0x0e01, 0x0e2e]);
|
|
590
|
+
reordered([0x0ec0, 0x0ec4], [0x0e81, 0x0eae]);
|
|
591
|
+
reordered([0x0ec0, 0x0ec4], [0x0edc, 0x0edf]);
|
|
592
|
+
for (const g of points) {
|
|
593
|
+
if (g === UNLISTED)
|
|
594
|
+
continue;
|
|
595
|
+
const ch = String.fromCodePoint(g);
|
|
596
|
+
for (const form of ["NFD", "NFKD"]) {
|
|
597
|
+
const d = cps(ch.normalize(form));
|
|
598
|
+
if (d.length < 2 || d.length > 3 || d.includes(0))
|
|
599
|
+
continue;
|
|
600
|
+
proposals.push([d, [g]]);
|
|
601
|
+
if (d.length === 3) {
|
|
602
|
+
const head = cps(str(d.slice(0, 2)).normalize("NFC"));
|
|
603
|
+
if (head.length === 1)
|
|
604
|
+
proposals.push([[head[0], d[2]], [g]]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const levels = (e) => JSON.stringify([e.l1, e.l2, e.l3]);
|
|
609
|
+
const known = (xs) => xs.every((x) => o.uncovered[x] === 0);
|
|
610
|
+
const seen = new Set();
|
|
611
|
+
const differ = proposals.filter(([p, t]) => {
|
|
612
|
+
const k = p.join(",");
|
|
613
|
+
if (seen.has(k) || !known(p) || !known(t) || levels(joined(p.map((x) => elementOf(o, x)))) === levels(joined(t.map((x) => elementOf(o, x)))))
|
|
614
|
+
return false;
|
|
615
|
+
seen.add(k);
|
|
616
|
+
return true;
|
|
617
|
+
});
|
|
618
|
+
const equal = await equalAt(differ.map(([p, t]) => [str(p), str(t)]));
|
|
619
|
+
differ.forEach(([p, t], i) => {
|
|
620
|
+
if (!equal[i])
|
|
621
|
+
return;
|
|
622
|
+
const list = o.contractions.get(p[0]) ?? [];
|
|
623
|
+
list.push({ rest: p.slice(1), element: joined(t.map((x) => elementOf(o, x))) });
|
|
624
|
+
list.sort((a, b) => b.rest.length - a.rest.length);
|
|
625
|
+
o.contractions.set(p[0], list);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
// what was not read: what the table does not list, one weight at the last level, when a sample says so; else not
|
|
629
|
+
// covered, but under the plain table, where it is that all the same and the sample's differences are recorded
|
|
630
|
+
let unreadDiffer = 0, unreadPairs = 0;
|
|
631
|
+
{
|
|
632
|
+
const read = new Set(points);
|
|
633
|
+
const unread = [];
|
|
634
|
+
for (let g = 1; g < MAX; g++)
|
|
635
|
+
if (!(g >= 0xd800 && g <= 0xdfff) && !read.has(g))
|
|
636
|
+
unread.push(g);
|
|
637
|
+
// of one length of UTF-8, held to one of them: two lengths differ at the last level by design
|
|
638
|
+
const firstOf = new Map([[3, UNLISTED]]);
|
|
639
|
+
for (const g of unread)
|
|
640
|
+
if (!firstOf.has(utf8Length(g)))
|
|
641
|
+
firstOf.set(utf8Length(g), g);
|
|
642
|
+
const [p, q] = [ignoredClasses[0], ignoredClasses[1] ?? ignoredClasses[0]];
|
|
643
|
+
const sample = unread.filter((_, i) => i % UNREAD_SAMPLE === 0).map((g) => [g, firstOf.get(utf8Length(g))]);
|
|
644
|
+
const alike = await asked(query, sample, `(chr(a) || 'y' > chr(b) || 'x') AND (chr(b) || 'y' > chr(a) || 'x') AND (chr(a) || 'X' > chr(b) || 'x') AND (chr(b) || 'X' > chr(a) || 'x')
|
|
645
|
+
AND (chr(a) || chr(${q}) > chr(b) || chr(${p})) AND (chr(b) || chr(${q}) > chr(a) || chr(${p}))`);
|
|
646
|
+
const plain = isPlainTable(collate);
|
|
647
|
+
if (plain) {
|
|
648
|
+
unreadPairs = alike.length;
|
|
649
|
+
unreadDiffer = alike.filter((x) => !x).length;
|
|
650
|
+
}
|
|
651
|
+
if (plain || (alike.every(Boolean) && o.l1[UNLISTED] === 0)) {
|
|
652
|
+
const weight = o.l4[UNLISTED];
|
|
653
|
+
if (weight > 0)
|
|
654
|
+
for (const { g } of noFirst)
|
|
655
|
+
if (o.l4[g] === weight)
|
|
656
|
+
o.unlisted[g] = 1;
|
|
657
|
+
for (const g of unread) {
|
|
658
|
+
o.l4[g] = weight;
|
|
659
|
+
o.back[g] = o.back[UNLISTED];
|
|
660
|
+
o.unlisted[g] = 1;
|
|
661
|
+
o.uncovered[g] = 0;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else
|
|
665
|
+
o.uncovered[UNLISTED] = 1;
|
|
666
|
+
}
|
|
667
|
+
withLookups(o);
|
|
668
|
+
await held();
|
|
669
|
+
o.held = { ...o.held, pairs: o.held.pairs + unreadPairs, differ: o.held.differ + unreadDiffer };
|
|
670
|
+
return o;
|
|
671
|
+
}
|
|
672
|
+
/** Pairs of strings of the covered characters, near each other as a column's values are, by `seed`. */
|
|
673
|
+
function heldPairs(o, seed) {
|
|
674
|
+
// a generator of its own, never Math.random: one source's order reads to one result
|
|
675
|
+
const next = (n) => {
|
|
676
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
677
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
678
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
679
|
+
return ((t ^ (t >>> 14)) >>> 0) % n;
|
|
680
|
+
};
|
|
681
|
+
const pool = [];
|
|
682
|
+
for (let g = 0x21; g < MAX; g++)
|
|
683
|
+
if (o.uncovered[g] === 0 && (o.unlisted[g] === 0 || g % 4099 === 0))
|
|
684
|
+
pool.push(g);
|
|
685
|
+
// (ASCII's own pool: a tailoring of punctuation or of `i` shows there first, in the values every column holds)
|
|
686
|
+
const ascii = [];
|
|
687
|
+
for (let g = 0x20; g < 0x7f; g++)
|
|
688
|
+
if (o.uncovered[g] === 0)
|
|
689
|
+
ascii.push(g);
|
|
690
|
+
const latin = pool.filter((g) => g < 0x250 && /\p{L}/u.test(String.fromCodePoint(g)));
|
|
691
|
+
const pairs = [];
|
|
692
|
+
const some = (pick) => {
|
|
693
|
+
for (let i = 0; i < HELD_PAIRS; i++) {
|
|
694
|
+
const a = Array.from({ length: 1 + next(4) }, pick);
|
|
695
|
+
const b = [...a];
|
|
696
|
+
b[next(b.length)] = pick();
|
|
697
|
+
if (next(2) === 0)
|
|
698
|
+
b.splice(next(b.length + 1), 0, pick());
|
|
699
|
+
pairs.push([str(a), str(b)]);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
const of = (xs) => () => xs[next(xs.length)];
|
|
703
|
+
const asciiOf = of(ascii), poolOf = of(pool);
|
|
704
|
+
some(() => next(3) === 0 ? asciiOf() : poolOf());
|
|
705
|
+
some(asciiOf);
|
|
706
|
+
some(of(latin));
|
|
707
|
+
// every two neighbours of the Latin letters alone, and of one first-level class, alone and between letters: what no
|
|
708
|
+
// random pair is likely to meet
|
|
709
|
+
const sortedLatin = latin.map((g) => String.fromCodePoint(g)).sort(keyOrder(o));
|
|
710
|
+
for (let i = 1; i < sortedLatin.length; i++)
|
|
711
|
+
pairs.push([sortedLatin[i - 1], sortedLatin[i]]);
|
|
712
|
+
const byFirst = new Map();
|
|
713
|
+
for (let g = 0x21; g < MAX; g++) {
|
|
714
|
+
if (o.uncovered[g] === 1 || o.unlisted[g] === 1)
|
|
715
|
+
continue;
|
|
716
|
+
const first = libcKey(o, String.fromCodePoint(g)).split(" ")[0];
|
|
717
|
+
if (first === "")
|
|
718
|
+
continue;
|
|
719
|
+
const list = byFirst.get(first);
|
|
720
|
+
if (list === undefined)
|
|
721
|
+
byFirst.set(first, [g]);
|
|
116
722
|
else
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
723
|
+
list.push(g);
|
|
724
|
+
}
|
|
725
|
+
for (const list of byFirst.values()) {
|
|
726
|
+
const sorted = list.map((g) => String.fromCodePoint(g)).sort(keyOrder(o));
|
|
727
|
+
for (let i = 1; i < sorted.length; i++)
|
|
728
|
+
for (const [before, after] of [["", ""], ["", "x"], ["x", ""]])
|
|
729
|
+
pairs.push([`${before}${sorted[i - 1]}${after}`, `${before}${sorted[i]}${after}`]);
|
|
730
|
+
}
|
|
731
|
+
return pairs;
|
|
732
|
+
}
|
|
733
|
+
/** below it, the characters most columns hold: ASCII and Latin. A tailoring that moves one is a table not followed */
|
|
734
|
+
const COMMON = 0x250;
|
|
735
|
+
/**
|
|
736
|
+
* WHAT WAS READ IS HELD, the verdict a branch (the module's header), in two rounds on two sets of pairs, every pair
|
|
737
|
+
* counted. The plain table only records. Under any other locale a pair that differs has its rarest character uncovered
|
|
738
|
+
* when one is not common (with it not covered, the other still is and the pair is gone), and a pair of common characters
|
|
739
|
+
* alone leaves nothing to uncover, so the table is not followed: a difference in ASCII or Latin is in every column.
|
|
740
|
+
*/
|
|
741
|
+
export async function holdLibcOrder(o, compare) {
|
|
742
|
+
const plain = isPlainTable(o.collate);
|
|
743
|
+
const held = { pairs: 0, differ: 0, common: 0 };
|
|
744
|
+
for (const seed of [0x9e3779b9, 0x51ed270b]) {
|
|
745
|
+
const pairs = heldPairs(o, seed);
|
|
746
|
+
const said = await compare(pairs);
|
|
747
|
+
const differ = pairs.filter(([s, t], i) => keyOrder(o)(s, t) !== said[i]);
|
|
748
|
+
held.pairs += pairs.length;
|
|
749
|
+
held.differ += differ.length;
|
|
750
|
+
for (const [s, t] of differ) {
|
|
751
|
+
const told = [...cps(s).filter((x) => !cps(t).includes(x)), ...cps(t).filter((x) => !cps(s).includes(x))];
|
|
752
|
+
const rare = told.filter((x) => x >= COMMON);
|
|
753
|
+
if (rare.length === 0)
|
|
754
|
+
held.common++;
|
|
755
|
+
if (plain)
|
|
756
|
+
continue;
|
|
757
|
+
if (rare.length > 0)
|
|
758
|
+
o.uncovered[Math.max(...rare)] = 1;
|
|
759
|
+
else
|
|
760
|
+
o.unread = true;
|
|
761
|
+
}
|
|
762
|
+
o.held = held;
|
|
763
|
+
if (o.unread)
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/** how two texts compare by their keys, a tie by their bytes (as Postgres breaks one) */
|
|
768
|
+
const keyOrder = (o) => (s, t) => {
|
|
769
|
+
const [x, y] = [libcKey(o, s), libcKey(o, t)];
|
|
770
|
+
return x < y ? -1 : x > y ? 1 : Math.sign(Buffer.compare(Buffer.from(s), Buffer.from(t)));
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* What an order's read leaves to say, for mason status and the log: an order not followed, or the pairs that differ,
|
|
774
|
+
* told into rare characters and common ones (the detector of a glibc that moved them). Null: nothing.
|
|
775
|
+
*/
|
|
776
|
+
export function libcOrderNote(o) {
|
|
777
|
+
if (o.bytes)
|
|
778
|
+
return null;
|
|
779
|
+
if (o.unread)
|
|
780
|
+
return `${o.collate} orders text in a way Mason could not read, so ORDER BY, <, min and max of text are refused (Mason gap libc-locale-unread); add COLLATE "C" or an ICU collation to them`;
|
|
781
|
+
if (o.held.differ === 0)
|
|
782
|
+
return null;
|
|
783
|
+
const rare = o.held.differ - o.held.common;
|
|
784
|
+
return `${o.collate} read from the database; ${o.held.differ.toLocaleString("en-US")} of ${o.held.pairs.toLocaleString("en-US")} pairs of strings held to it order otherwise: ` +
|
|
785
|
+
`${rare.toLocaleString("en-US")} of rare characters, ${o.held.common.toLocaleString("en-US")} of ASCII or Latin alone (Mason gaps libc-rare-letter, libc-mark-run-form)`;
|
|
786
|
+
}
|
|
787
|
+
/** The refusal of every text under an order `holdLibcOrder` did not follow: by the gap and the collation, as `libc-uncovered`'s. */
|
|
788
|
+
const unreadRefusal = (o) => new PgEvalError("0A000", `the database's collation ${o.collate} orders text in a way Mason could not read, so text under it is not ordered (${gapMark("libc-locale-unread")})`);
|
|
789
|
+
const utf8Length = (g) => g < 0x80 ? 1 : g < 0x800 ? 2 : g < 0x10000 ? 3 : 4;
|
|
790
|
+
const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
791
|
+
const hex6 = (n) => HEX[(n >>> 16) & 0xff] + HEX[(n >>> 8) & 0xff] + HEX[n & 0xff];
|
|
792
|
+
/**
|
|
793
|
+
* A level of weights as text that orders as the level does, its common weight (1, the lowest) counted, so a value of
|
|
794
|
+
* plain letters costs one token: a run of n of it before another weight w is `1`, the complement of n (a longer run has
|
|
795
|
+
* the lower weight where a shorter one ended) and w; the run the level ends with is `0` and n (below any `1`, the shorter
|
|
796
|
+
* first).
|
|
797
|
+
*/
|
|
798
|
+
function level(xs) {
|
|
799
|
+
let out = "", run = 0;
|
|
800
|
+
for (let i = 0; i < xs.length; i++) {
|
|
801
|
+
const x = xs[i];
|
|
802
|
+
if (x === 1) {
|
|
803
|
+
run++;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
out += `1${hex6(0xffffff - run)}${hex6(x)}`;
|
|
807
|
+
run = 0;
|
|
808
|
+
}
|
|
809
|
+
return `${out}0${hex6(run)}`;
|
|
145
810
|
}
|
|
146
811
|
/**
|
|
147
812
|
* The sort key of `s` as one text that orders as glibc orders (bytewise; a space parts the levels, below every hex digit,
|
|
148
|
-
* so a shorter level sorts first): the
|
|
149
|
-
*
|
|
813
|
+
* so a shorter level sorts first): the module's header's four levels. A value holding a character not covered is
|
|
814
|
+
* refused, and every value of an order not followed, by the gap alone.
|
|
150
815
|
*/
|
|
151
|
-
export function libcKey(o, s
|
|
816
|
+
export function libcKey(o, s) {
|
|
817
|
+
if (o.unread)
|
|
818
|
+
throw unreadRefusal(o);
|
|
152
819
|
let first = "", last = "", at = 0;
|
|
153
|
-
|
|
820
|
+
const second = [], third = [];
|
|
821
|
+
const run = [];
|
|
822
|
+
const push = (e) => {
|
|
823
|
+
if (typeof e !== "number") {
|
|
824
|
+
for (const x of e.l2)
|
|
825
|
+
second.push(x);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const x = o.l2[e];
|
|
829
|
+
if (x > 0)
|
|
830
|
+
second.push(x);
|
|
831
|
+
};
|
|
832
|
+
// glibc's run read in reverse; closed by a forward element, it skips the element before its last
|
|
833
|
+
const flush = (closed) => {
|
|
834
|
+
const n = run.length;
|
|
835
|
+
for (let k = n - 1; k >= 0; k--)
|
|
836
|
+
if (!(closed && n >= 2 && k === n - 2))
|
|
837
|
+
push(run[k]);
|
|
838
|
+
run.length = 0;
|
|
839
|
+
};
|
|
840
|
+
for (let i = 0; i < s.length;) {
|
|
841
|
+
const g = s.codePointAt(i);
|
|
842
|
+
i += g > 0xffff ? 2 : 1;
|
|
154
843
|
at++;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
throw new PgEvalError("0A000", `
|
|
844
|
+
if (o.uncovered[g] === 1) {
|
|
845
|
+
// never the value, nor the character: a refusal reaches any user, of a row a role may hide (issue 128)
|
|
846
|
+
throw new PgEvalError("0A000", `a text being ordered holds a character Mason cannot order as the database's collation ${o.collate} does (${gapMark("libc-uncovered")})`);
|
|
847
|
+
}
|
|
848
|
+
if (o.special[g] === 0) {
|
|
849
|
+
const w = o.l1[g];
|
|
850
|
+
if (w > 0)
|
|
851
|
+
first += hex6(w);
|
|
852
|
+
const c = o.l3[g];
|
|
853
|
+
if (c > 0)
|
|
854
|
+
third.push(c);
|
|
855
|
+
const p = o.l4[g];
|
|
856
|
+
if (p > 0)
|
|
857
|
+
last += hex6(at) + hex6(p);
|
|
858
|
+
if (o.back[g] === 1)
|
|
859
|
+
run.push(g);
|
|
860
|
+
else {
|
|
861
|
+
if (run.length > 0)
|
|
862
|
+
flush(true);
|
|
863
|
+
push(g);
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (o.unlisted[g] === 1) {
|
|
868
|
+
if (run.length > 0 && o.back[g] === 0)
|
|
869
|
+
flush(true);
|
|
870
|
+
for (let n = utf8Length(g); n > 0; n--) {
|
|
871
|
+
last += hex6(at) + hex6(o.l4[g]);
|
|
872
|
+
if (n > 1)
|
|
873
|
+
at++;
|
|
874
|
+
if (o.back[g] === 1)
|
|
875
|
+
run.push(g);
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
let e = o.sequences.get(g);
|
|
880
|
+
for (const c of o.contractions.get(g) ?? []) {
|
|
881
|
+
let j = i, whole = true;
|
|
882
|
+
for (const r of c.rest) {
|
|
883
|
+
if (s.codePointAt(j) !== r) {
|
|
884
|
+
whole = false;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
j += r > 0xffff ? 2 : 1;
|
|
888
|
+
}
|
|
889
|
+
if (whole) {
|
|
890
|
+
e = c.element;
|
|
891
|
+
i = j;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (e === undefined)
|
|
896
|
+
e = elementOf(o, g);
|
|
897
|
+
for (const w of e.l1)
|
|
898
|
+
first += hex6(w);
|
|
899
|
+
for (const c of e.l3)
|
|
900
|
+
third.push(c);
|
|
901
|
+
for (let k = 0; k < e.l4.length; k++) {
|
|
902
|
+
if (k > 0)
|
|
903
|
+
at++;
|
|
904
|
+
last += hex6(at) + hex6(e.l4[k]);
|
|
905
|
+
}
|
|
906
|
+
if (e.back)
|
|
907
|
+
run.push(e);
|
|
908
|
+
else {
|
|
909
|
+
if (run.length > 0)
|
|
910
|
+
flush(true);
|
|
911
|
+
push(e);
|
|
158
912
|
}
|
|
159
|
-
if (o.l1[g] > 0)
|
|
160
|
-
for (const x of o.expansions.get(g) ?? [g])
|
|
161
|
-
if (o.l1[x] > 0)
|
|
162
|
-
first += hex6(o.l1[x]);
|
|
163
|
-
if (o.l4[g] > 0)
|
|
164
|
-
last += hex6(at) + hex6(o.l4[g]);
|
|
165
913
|
}
|
|
166
|
-
|
|
914
|
+
if (run.length > 0)
|
|
915
|
+
flush(false);
|
|
916
|
+
return `${first} ${level(second)} ${level(third)} ${last}`;
|
|
167
917
|
}
|
|
168
|
-
/**
|
|
918
|
+
/** an array as its runs of one value, 0 left out: the order is mostly empty, and one value past the assigned code points */
|
|
919
|
+
function runsOf(a) {
|
|
920
|
+
const out = [];
|
|
921
|
+
for (let g = 0; g < MAX;) {
|
|
922
|
+
const v = a[g];
|
|
923
|
+
let end = g + 1;
|
|
924
|
+
while (end < MAX && a[end] === v)
|
|
925
|
+
end++;
|
|
926
|
+
if (v !== 0)
|
|
927
|
+
out.push([g, end - g, v]);
|
|
928
|
+
g = end;
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
const fromRuns = (a, runs) => { for (const [g, n, v] of runs)
|
|
933
|
+
a.fill(v, g, g + n); };
|
|
934
|
+
/** what `encodeLibcOrder` says first: an order kept by a release that read less is read again (`isCurrentLibcOrder`) */
|
|
935
|
+
const FORM = '{"form":4,';
|
|
936
|
+
export const isCurrentLibcOrder = (text) => text.startsWith(FORM);
|
|
937
|
+
/** The order as bytes, to keep in the copy. */
|
|
169
938
|
export function encodeLibcOrder(o) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const out = [];
|
|
173
|
-
for (let g = 0; g < MAX; g++)
|
|
174
|
-
if (a[g] !== skip)
|
|
175
|
-
out.push([g, a[g]]);
|
|
176
|
-
return out;
|
|
177
|
-
};
|
|
178
|
-
return JSON.stringify({ collate: o.collate, version: o.version, l1: sparse(o.l1, 0), l4: sparse(o.l4, 0), covered: sparse(o.uncovered, 1).map(([g]) => g), expansions: [...o.expansions] });
|
|
939
|
+
return FORM + JSON.stringify({ collate: o.collate, version: o.version, bytes: o.bytes, unread: o.unread, held: o.held, l1: runsOf(o.l1), l2: runsOf(o.l2), l3: runsOf(o.l3), l4: runsOf(o.l4), back: runsOf(o.back), uncovered: runsOf(o.uncovered), unlisted: runsOf(o.unlisted),
|
|
940
|
+
sequences: [...o.sequences], contractions: [...o.contractions] }).slice(1);
|
|
179
941
|
}
|
|
180
942
|
export function decodeLibcOrder(text) {
|
|
181
943
|
const v = JSON.parse(text);
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
944
|
+
const o = { ...emptyLibcOrder(v.collate, v.version), bytes: v.bytes, unread: v.unread, held: v.held };
|
|
945
|
+
o.uncovered.fill(0);
|
|
946
|
+
fromRuns(o.l1, v.l1);
|
|
947
|
+
fromRuns(o.l2, v.l2);
|
|
948
|
+
fromRuns(o.l3, v.l3);
|
|
949
|
+
fromRuns(o.l4, v.l4);
|
|
950
|
+
fromRuns(o.back, v.back);
|
|
951
|
+
fromRuns(o.uncovered, v.uncovered);
|
|
952
|
+
fromRuns(o.unlisted, v.unlisted);
|
|
953
|
+
o.sequences = new Map(v.sequences);
|
|
954
|
+
o.contractions = new Map(v.contractions);
|
|
955
|
+
return withLookups(o);
|
|
190
956
|
}
|