@volter/twin-planetscale 0.1.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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +473 -0
  3. package/api/src/fetch.ts +50 -0
  4. package/api/src/generated/surface.gen.json +1 -0
  5. package/api/src/generated/ui.gen.json +1 -0
  6. package/api/src/index.ts +19 -0
  7. package/api/src/manifest.ts +136 -0
  8. package/api/src/screens/deploy-request.tsx +111 -0
  9. package/api/src/screens/service-tokens.tsx +141 -0
  10. package/api/src/screens/session.tsx +117 -0
  11. package/api/src/semantics/audit.ts +82 -0
  12. package/api/src/semantics/backups.ts +258 -0
  13. package/api/src/semantics/branches.ts +201 -0
  14. package/api/src/semantics/deploy-requests.ts +493 -0
  15. package/api/src/semantics/index.ts +371 -0
  16. package/api/src/semantics/shared.ts +141 -0
  17. package/api/src/semantics/time.ts +77 -0
  18. package/api/src/token-gate.ts +96 -0
  19. package/dist/api/src/fetch.d.ts +8 -0
  20. package/dist/api/src/fetch.js +51 -0
  21. package/dist/api/src/fetch.ts +50 -0
  22. package/dist/api/src/generated/surface.gen.json +1 -0
  23. package/dist/api/src/generated/ui.gen.json +1 -0
  24. package/dist/api/src/index.ts +19 -0
  25. package/dist/api/src/manifest.d.ts +2 -0
  26. package/dist/api/src/manifest.js +113 -0
  27. package/dist/api/src/manifest.ts +136 -0
  28. package/dist/api/src/screens/deploy-request.d.ts +7 -0
  29. package/dist/api/src/screens/deploy-request.js +106 -0
  30. package/dist/api/src/screens/deploy-request.tsx +111 -0
  31. package/dist/api/src/screens/service-tokens.d.ts +3 -0
  32. package/dist/api/src/screens/service-tokens.js +134 -0
  33. package/dist/api/src/screens/service-tokens.tsx +141 -0
  34. package/dist/api/src/screens/session.d.ts +11 -0
  35. package/dist/api/src/screens/session.js +108 -0
  36. package/dist/api/src/screens/session.tsx +117 -0
  37. package/dist/api/src/semantics/audit.d.ts +31 -0
  38. package/dist/api/src/semantics/audit.js +80 -0
  39. package/dist/api/src/semantics/audit.ts +82 -0
  40. package/dist/api/src/semantics/backups.d.ts +37 -0
  41. package/dist/api/src/semantics/backups.js +264 -0
  42. package/dist/api/src/semantics/backups.ts +258 -0
  43. package/dist/api/src/semantics/branches.d.ts +53 -0
  44. package/dist/api/src/semantics/branches.js +197 -0
  45. package/dist/api/src/semantics/branches.ts +201 -0
  46. package/dist/api/src/semantics/deploy-requests.d.ts +47 -0
  47. package/dist/api/src/semantics/deploy-requests.js +491 -0
  48. package/dist/api/src/semantics/deploy-requests.ts +493 -0
  49. package/dist/api/src/semantics/index.d.ts +20 -0
  50. package/dist/api/src/semantics/index.js +381 -0
  51. package/dist/api/src/semantics/index.ts +371 -0
  52. package/dist/api/src/semantics/shared.d.ts +36 -0
  53. package/dist/api/src/semantics/shared.js +132 -0
  54. package/dist/api/src/semantics/shared.ts +141 -0
  55. package/dist/api/src/semantics/time.d.ts +2 -0
  56. package/dist/api/src/semantics/time.js +81 -0
  57. package/dist/api/src/semantics/time.ts +77 -0
  58. package/dist/api/src/token-gate.d.ts +9 -0
  59. package/dist/api/src/token-gate.js +97 -0
  60. package/dist/api/src/token-gate.ts +96 -0
  61. package/dist/src/cli.d.ts +2 -0
  62. package/dist/src/cli.js +61 -0
  63. package/dist/src/generated/surface.gen.json +1 -0
  64. package/dist/src/generated/ui.gen.json +1 -0
  65. package/dist/src/index.d.ts +26 -0
  66. package/dist/src/index.js +156 -0
  67. package/dist/src/manifest.d.ts +2 -0
  68. package/dist/src/manifest.js +41 -0
  69. package/dist/src/planetscale-budget.d.ts +78 -0
  70. package/dist/src/planetscale-budget.js +305 -0
  71. package/dist/src/planetscale-capabilities.d.ts +10 -0
  72. package/dist/src/planetscale-capabilities.js +3977 -0
  73. package/dist/src/planetscale-collation-weights.gen.d.ts +4 -0
  74. package/dist/src/planetscale-collation-weights.gen.js +12 -0
  75. package/dist/src/planetscale-collation.d.ts +70 -0
  76. package/dist/src/planetscale-collation.js +391 -0
  77. package/dist/src/planetscale-conformance.d.ts +8 -0
  78. package/dist/src/planetscale-conformance.js +213 -0
  79. package/dist/src/planetscale-connector.d.ts +150 -0
  80. package/dist/src/planetscale-connector.js +532 -0
  81. package/dist/src/planetscale-deploy.d.ts +26 -0
  82. package/dist/src/planetscale-deploy.js +235 -0
  83. package/dist/src/planetscale-information-schema.d.ts +32 -0
  84. package/dist/src/planetscale-information-schema.js +299 -0
  85. package/dist/src/planetscale-mysql.d.ts +33 -0
  86. package/dist/src/planetscale-mysql.js +547 -0
  87. package/dist/src/planetscale-roles.d.ts +11 -0
  88. package/dist/src/planetscale-roles.js +60 -0
  89. package/dist/src/planetscale-row.d.ts +12 -0
  90. package/dist/src/planetscale-row.js +39 -0
  91. package/dist/src/planetscale-server.d.ts +42 -0
  92. package/dist/src/planetscale-server.js +137 -0
  93. package/dist/src/planetscale-sql.d.ts +701 -0
  94. package/dist/src/planetscale-sql.js +7167 -0
  95. package/dist/src/planetscale-store.d.ts +126 -0
  96. package/dist/src/planetscale-store.js +827 -0
  97. package/dist/src/planetscale-twin.d.ts +48 -0
  98. package/dist/src/planetscale-twin.js +290 -0
  99. package/dist/src/planetscale-values.d.ts +139 -0
  100. package/dist/src/planetscale-values.js +719 -0
  101. package/dist/src/planetscale-wire.d.ts +110 -0
  102. package/dist/src/planetscale-wire.js +188 -0
  103. package/dist/src/semantics/psdb.d.ts +18 -0
  104. package/dist/src/semantics/psdb.js +30 -0
  105. package/package.json +58 -0
  106. package/src/cli.ts +58 -0
  107. package/src/generated/surface.gen.json +1 -0
  108. package/src/generated/ui.gen.json +1 -0
  109. package/src/index.ts +267 -0
  110. package/src/manifest.ts +60 -0
  111. package/src/planetscale-budget.ts +347 -0
  112. package/src/planetscale-capabilities.ts +3862 -0
  113. package/src/planetscale-collation-weights.gen.ts +13 -0
  114. package/src/planetscale-collation.ts +378 -0
  115. package/src/planetscale-conformance.ts +237 -0
  116. package/src/planetscale-connector.ts +571 -0
  117. package/src/planetscale-deploy.ts +197 -0
  118. package/src/planetscale-information-schema.ts +322 -0
  119. package/src/planetscale-mysql.ts +339 -0
  120. package/src/planetscale-roles.ts +71 -0
  121. package/src/planetscale-row.ts +43 -0
  122. package/src/planetscale-server.ts +162 -0
  123. package/src/planetscale-sql.ts +5957 -0
  124. package/src/planetscale-store.ts +869 -0
  125. package/src/planetscale-twin.ts +338 -0
  126. package/src/planetscale-values.ts +572 -0
  127. package/src/planetscale-wire.ts +274 -0
  128. package/src/semantics/psdb.ts +57 -0
@@ -0,0 +1,719 @@
1
+ // MySQL 8.0 VALUES — the arithmetic, text forms and orderings of the types a cell can hold. The
2
+ // engine stores every cell as MySQL's text form of its value (`planetscale-sql.ts`, `Cell`); this
3
+ // module is what makes that text a TYPED value again: exact decimals (manual 14.24 "Precision
4
+ // Math"), doubles printed the way the server prints them, temporals parsed from every literal form
5
+ // MySQL accepts and compared as points in time (13.2 "Date and Time Data Types"), and JSON
6
+ // documents parsed with exact integers, printed normalized and compared by MySQL's JSON ordering
7
+ // (13.5 "The JSON Data Type", "Comparison and Ordering of JSON Values").
8
+ //
9
+ // Pure: no clock, no I/O. Nothing here knows about tables or SQL syntax.
10
+ const TEN = 10n;
11
+ const pow10 = (n) => TEN ** BigInt(n);
12
+ const DECIMAL_TEXT = /^\s*([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?\s*$/;
13
+ /** A plain decimal or exponent spelling (`12`, `-0.50`, `.5`, `1.5e3`) as an exact value; null for
14
+ * anything else. */
15
+ export function parseDec(text) {
16
+ const m = DECIMAL_TEXT.exec(text);
17
+ if (m === null || (m[2] === '' && (m[3] ?? '') === ''))
18
+ return null;
19
+ const frac = m[3] ?? '';
20
+ let mant = BigInt(`${m[2] || '0'}${frac}`);
21
+ let s = frac.length;
22
+ const exp = m[4] === undefined ? 0 : Number(m[4]);
23
+ if (Math.abs(exp) > 400)
24
+ return null;
25
+ if (exp > 0) {
26
+ if (exp >= s) {
27
+ mant *= pow10(exp - s);
28
+ s = 0;
29
+ }
30
+ else
31
+ s -= exp;
32
+ }
33
+ else if (exp < 0)
34
+ s -= exp;
35
+ return { m: m[1] === '-' ? -mant : mant, s };
36
+ }
37
+ /** A double as the exact decimal of its shortest round-trip spelling. */
38
+ export function decFromNumber(n) {
39
+ if (!Number.isFinite(n))
40
+ return { m: 0n, s: 0 };
41
+ return parseDec(n.toExponential()) ?? { m: 0n, s: 0 };
42
+ }
43
+ export const decFromBigInt = (n) => ({ m: n, s: 0 });
44
+ function align(a, b) {
45
+ const s = Math.max(a.s, b.s);
46
+ return [a.m * pow10(s - a.s), b.m * pow10(s - b.s), s];
47
+ }
48
+ export const decAdd = (a, b) => { const [x, y, s] = align(a, b); return { m: x + y, s }; };
49
+ export const decSub = (a, b) => { const [x, y, s] = align(a, b); return { m: x - y, s }; };
50
+ export const decMul = (a, b) => ({ m: a.m * b.m, s: a.s + b.s });
51
+ export const decNeg = (a) => ({ m: -a.m, s: a.s });
52
+ export const decIsZero = (a) => a.m === 0n;
53
+ export function decCmp(a, b) { const [x, y] = align(a, b); return x < y ? -1 : x > y ? 1 : 0; }
54
+ /** Round half away from zero to `scale` digits (MySQL's rounding for exact values). */
55
+ export function decRound(a, scale) {
56
+ if (a.s <= scale)
57
+ return { m: a.m * pow10(scale - a.s), s: scale };
58
+ const div = pow10(a.s - scale);
59
+ const neg = a.m < 0n;
60
+ const abs = neg ? -a.m : a.m;
61
+ let q = abs / div;
62
+ const r = abs % div;
63
+ if (r * 2n >= div)
64
+ q += 1n;
65
+ return { m: neg ? -q : q, s: scale };
66
+ }
67
+ /** Truncate toward zero to an integer. */
68
+ export const decTrunc = (a) => a.m / pow10(a.s);
69
+ /** `a / b` rounded half away from zero to `scale` digits; null on division by zero. */
70
+ export function decDiv(a, b, scale) {
71
+ if (b.m === 0n)
72
+ return null;
73
+ // a/b = (a.m / 10^a.s) / (b.m / 10^b.s); scale the numerator so the quotient carries scale+1 digits.
74
+ const extra = scale + 1 + b.s - a.s;
75
+ let num = a.m;
76
+ let den = b.m;
77
+ if (extra >= 0)
78
+ num *= pow10(extra);
79
+ else
80
+ den *= pow10(-extra);
81
+ const neg = (num < 0n) !== (den < 0n);
82
+ const q = (num < 0n ? -num : num) / (den < 0n ? -den : den);
83
+ return decRound({ m: neg ? -q : q, s: scale + 1 }, scale);
84
+ }
85
+ /** The remainder of `a / b` with the sign of `a` (MySQL MOD); null on division by zero. */
86
+ export function decMod(a, b) {
87
+ if (b.m === 0n)
88
+ return null;
89
+ const [x, y, s] = align(a, b);
90
+ return { m: x % y, s };
91
+ }
92
+ /** The text of an exact value at its own scale, or at `scale` when given (padded or rounded). */
93
+ export function decText(a, scale) {
94
+ const v = scale === undefined ? a : decRound(a, scale);
95
+ const neg = v.m < 0n;
96
+ const digits = (neg ? -v.m : v.m).toString().padStart(v.s + 1, '0');
97
+ const text = v.s === 0 ? digits : `${digits.slice(0, -v.s)}.${digits.slice(-v.s)}`;
98
+ return neg && v.m !== 0n ? `-${text}` : text;
99
+ }
100
+ /** The canonical key of a value: trailing fractional zeros removed, so 1.50 and 1.5 meet. */
101
+ export function decKey(a) {
102
+ let { m, s } = a;
103
+ while (s > 0 && m % TEN === 0n) {
104
+ m /= TEN;
105
+ s--;
106
+ }
107
+ return decText({ m, s });
108
+ }
109
+ /** Digits before the decimal point (0 for a pure fraction). */
110
+ export function decIntDigits(a) {
111
+ const i = decTrunc(a);
112
+ const abs = i < 0n ? -i : i;
113
+ return abs === 0n ? 0 : abs.toString().length;
114
+ }
115
+ export const decToNumber = (a) => Number(decText(a));
116
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
117
+ // DOUBLES
118
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
119
+ /**
120
+ * A double as MySQL prints one: the shortest digits that round-trip, in positional notation while
121
+ * the decimal exponent stays within DBL_DIG (15) digits, else `d.ddde±x` without a plus sign
122
+ * (`1e20`, `1.2345678901234568e17`, `1e-7`).
123
+ */
124
+ export function formatDouble(n) {
125
+ if (Object.is(n, -0))
126
+ return '-0';
127
+ if (n === 0)
128
+ return '0';
129
+ const [mant, expText] = n.toExponential().split('e');
130
+ const exp = Number(expText);
131
+ const neg = mant.startsWith('-');
132
+ const digits = mant.replace('-', '').replace('.', '');
133
+ const decpt = exp + 1;
134
+ let out;
135
+ if (decpt > 15 || decpt < -5) {
136
+ out = `${digits[0]}${digits.length > 1 ? `.${digits.slice(1)}` : ''}e${exp}`;
137
+ }
138
+ else if (decpt <= 0) {
139
+ out = `0.${'0'.repeat(-decpt)}${digits}`;
140
+ }
141
+ else if (decpt >= digits.length) {
142
+ out = digits + '0'.repeat(decpt - digits.length);
143
+ }
144
+ else {
145
+ out = `${digits.slice(0, decpt)}.${digits.slice(decpt)}`;
146
+ }
147
+ return neg ? `-${out}` : out;
148
+ }
149
+ /** A FLOAT (single precision) as MySQL prints it: the shortest digits that round-trip as float32. */
150
+ export function formatFloat(n) {
151
+ const f = Math.fround(n);
152
+ if (f === 0)
153
+ return Object.is(f, -0) ? '-0' : '0';
154
+ for (let p = 1; p <= 9; p++) {
155
+ const text = f.toPrecision(p);
156
+ if (Math.fround(Number(text)) === f)
157
+ return formatDouble(Number(text));
158
+ }
159
+ return formatDouble(f);
160
+ }
161
+ /** MySQL's string → number conversion: the longest numeric prefix after leading spaces (`'1abc'`
162
+ * is 1, `'abc'` is 0), with `complete` saying whether the whole string was that number. */
163
+ export function stringNumberPrefix(text) {
164
+ const m = /^[ \t\n\r]*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/.exec(text);
165
+ if (m === null)
166
+ return { value: 0, text: '0', complete: false };
167
+ const rest = text.slice(m[0].length);
168
+ return { value: Number(m[1]), text: m[1], complete: /^[ \t\n\r]*$/.test(rest) };
169
+ }
170
+ const isLeap = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
171
+ const daysIn = (y, mo) => [31, isLeap(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1] ?? 0;
172
+ const twoDigitYear = (y) => (y < 70 ? 2000 + y : 1900 + y);
173
+ /** Microseconds from a fraction's digits, rounded to six digits (a seventh digit rounds). */
174
+ function fractionMicros(digits) {
175
+ if (digits === '')
176
+ return { us: 0, carry: false };
177
+ let us = Number(digits.slice(0, 6).padEnd(6, '0'));
178
+ if (digits.length > 6 && digits[6] >= '5')
179
+ us += 1;
180
+ return us >= 1_000_000 ? { us: 0, carry: true } : { us, carry: false };
181
+ }
182
+ /** A date MySQL reads: a real date, or an incomplete one with a zero month or day ("MySQL permits the storing of
183
+ * incomplete dates such as '2014-00-00'", manual 14.7, DATE_FORMAT), which the `zero` flag carries to a strict write. */
184
+ function validDate(t) {
185
+ if (t.y > 9999 || t.mo < 0 || t.mo > 12 || t.d < 0)
186
+ return false;
187
+ if (t.mo === 0 || t.d === 0)
188
+ return t.d <= 31;
189
+ return t.d <= daysIn(t.y, t.mo);
190
+ }
191
+ function validClock(t) { return t.h >= 0 && t.h <= 23 && t.mi >= 0 && t.mi <= 59 && t.s >= 0 && t.s <= 59; }
192
+ /**
193
+ * A DATE / DATETIME / TIMESTAMP string in any form MySQL accepts (manual 11.1.3 "Date and Time
194
+ * Literals"): delimited `YYYY-MM-DD[ hh:mm:ss[.fraction]]` with any punctuation as the delimiter
195
+ * and a space or `T` between date and time (the `T` form is what @planetscale/database sends for a
196
+ * `Date`), two-digit years (70–99 → 19xx, 00–69 → 20xx), the undelimited `YYYYMMDD[hhmmss]` and
197
+ * `YYMMDD[hhmmss]` forms, and a trailing `+hh:mm`/`-hh:mm` offset (8.0.19+), converted to UTC, the
198
+ * session time zone. Null when the text is not a date.
199
+ */
200
+ export function parseDateTimeText(text) {
201
+ const src = text.replace(/^\s+/, '');
202
+ const m = /^(\d{1,4})[^\d\sT](\d{1,2})[^\d\sT](\d{1,2})/.exec(src);
203
+ let t;
204
+ let rest;
205
+ if (m !== null) {
206
+ const y = m[1].length <= 2 ? twoDigitYear(Number(m[1])) : Number(m[1]);
207
+ t = { y, mo: Number(m[2]), d: Number(m[3]), h: 0, mi: 0, s: 0, us: 0, neg: false };
208
+ rest = src.slice(m[0].length);
209
+ const time = /^(?:[T ]|\s+)(\d{1,2})[^\d\s](\d{1,2})(?:[^\d\s](\d{1,2}))?(?:\.(\d*))?/.exec(rest);
210
+ if (time !== null) {
211
+ t.h = Number(time[1]);
212
+ t.mi = Number(time[2]);
213
+ t.s = Number(time[3] ?? 0);
214
+ const f = fractionMicros(time[4] ?? '');
215
+ t.us = f.us;
216
+ if (f.carry)
217
+ addMicros(t, 1_000_000);
218
+ rest = rest.slice(time[0].length);
219
+ }
220
+ }
221
+ else {
222
+ // Undelimited: all digits, optionally with a fraction.
223
+ const d = /^(\d{14}|\d{12}|\d{8}|\d{6})(?:\.(\d*))?/.exec(src);
224
+ if (d === null)
225
+ return null;
226
+ const s = d[1];
227
+ const long = s.length === 14 || s.length === 8;
228
+ const yLen = long ? 4 : 2;
229
+ const y = long ? Number(s.slice(0, 4)) : twoDigitYear(Number(s.slice(0, 2)));
230
+ t = { y, mo: Number(s.slice(yLen, yLen + 2)), d: Number(s.slice(yLen + 2, yLen + 4)), h: 0, mi: 0, s: 0, us: 0, neg: false };
231
+ if (s.length === 14 || s.length === 12) {
232
+ const at = yLen + 4;
233
+ t.h = Number(s.slice(at, at + 2));
234
+ t.mi = Number(s.slice(at + 2, at + 4));
235
+ t.s = Number(s.slice(at + 4, at + 6));
236
+ }
237
+ const f = fractionMicros(d[2] ?? '');
238
+ t.us = f.us;
239
+ if (f.carry)
240
+ addMicros(t, 1_000_000);
241
+ rest = src.slice(d[0].length);
242
+ }
243
+ if (!validDate(t) || !validClock(t))
244
+ return null;
245
+ // a trailing offset, `+hh:mm` (one or two hour digits, as the manual's own '2020-07-28 14:50:15+1:00' writes it)
246
+ const offset = /^([+-])(\d{1,2}):(\d{2})/.exec(rest);
247
+ if (offset !== null) {
248
+ const minutes = Number(offset[2]) * 60 + Number(offset[3]);
249
+ if (minutes > 14 * 60 || (offset[1] === '-' && minutes > 13 * 60 + 59))
250
+ return null;
251
+ addMicros(t, (offset[1] === '-' ? 1 : -1) * minutes * 60_000_000);
252
+ rest = rest.slice(offset[0].length);
253
+ }
254
+ const zero = t.y === 0 || t.mo === 0 || t.d === 0;
255
+ return { t, truncated: !/^\s*$/.test(rest), zero };
256
+ }
257
+ /** A TIME string (manual 11.1.3): `[-][D ]hh:mm[:ss][.fraction]`, `hh:mm`, or undelimited
258
+ * `[-]hhmmss`/`mmss`/`ss`. A full DATETIME string is taken as its time of day. Range ±838:59:59. */
259
+ export function parseTimeText(text) {
260
+ const src = text.trim();
261
+ const full = /^\d{4}-\d{1,2}-\d{1,2}[T ]/.test(src) ? parseDateTimeText(src) : null;
262
+ if (full !== null)
263
+ return { t: { ...full.t, y: 0, mo: 0, d: 0 }, truncated: full.truncated, zero: false };
264
+ let m = /^(-)?(?:(\d+)\s+)?(\d{1,3}):(\d{1,2})(?::(\d{1,2}))?(?:\.(\d*))?/.exec(src);
265
+ let t;
266
+ let consumed;
267
+ if (m !== null) {
268
+ const hours = Number(m[2] ?? 0) * 24 + Number(m[3]);
269
+ t = { y: 0, mo: 0, d: 0, h: hours, mi: Number(m[4]), s: Number(m[5] ?? 0), us: 0, neg: m[1] === '-' };
270
+ const f = fractionMicros(m[6] ?? '');
271
+ t.us = f.us;
272
+ if (f.carry)
273
+ t.s += 1;
274
+ consumed = m[0].length;
275
+ }
276
+ else {
277
+ m = /^(-)?(\d+)(?:\.(\d*))?/.exec(src);
278
+ if (m === null)
279
+ return null;
280
+ const digits = m[2].padStart(6, '0');
281
+ t = { y: 0, mo: 0, d: 0, h: Number(digits.slice(0, -4)), mi: Number(digits.slice(-4, -2)), s: Number(digits.slice(-2)), us: 0, neg: m[1] === '-' };
282
+ const f = fractionMicros(m[3] ?? '');
283
+ t.us = f.us;
284
+ if (f.carry)
285
+ t.s += 1;
286
+ consumed = m[0].length;
287
+ }
288
+ if (t.mi > 59 || t.s > 59)
289
+ return null;
290
+ if (t.h > 838 || (t.h === 838 && (t.mi > 59 || t.s > 59)))
291
+ return null;
292
+ return { t, truncated: !/^\s*$/.test(src.slice(consumed)), zero: false };
293
+ }
294
+ /** A number used as a temporal (`20240101`, `20240101103000.5`, `103000` for a TIME). */
295
+ export function parseTemporalNumber(text, kind) {
296
+ const m = /^\s*(-)?(\d+)(?:\.(\d*))?\s*$/.exec(text);
297
+ if (m === null)
298
+ return null;
299
+ if (kind === 'TIME')
300
+ return parseTimeText(text);
301
+ const digits = m[2];
302
+ if (m[1] === '-')
303
+ return null;
304
+ if (digits === '0')
305
+ return { t: { y: 0, mo: 0, d: 0, h: 0, mi: 0, s: 0, us: 0, neg: false }, truncated: false, zero: true };
306
+ const padded = digits.length <= 6 ? digits.padStart(6, '0') : digits.length <= 8 ? digits.padStart(8, '0')
307
+ : digits.length <= 12 ? digits.padStart(12, '0') : digits.padStart(14, '0');
308
+ return parseDateTimeText(`${padded}${m[3] !== undefined ? `.${m[3]}` : ''}`);
309
+ }
310
+ /** The UTC instant of a proleptic Gregorian wall time (years 0–99 included, which `Date.UTC` maps to 19xx). */
311
+ function utcMs(t) {
312
+ const at = new Date(0);
313
+ at.setUTCFullYear(t.y, t.mo - 1, t.d);
314
+ at.setUTCHours(t.h, t.mi, t.s, 0);
315
+ return at.getTime();
316
+ }
317
+ /** Add microseconds to a date-time, carrying through days, months and years. */
318
+ function addMicros(t, micros) {
319
+ const total = t.us + micros;
320
+ const carrySeconds = Math.floor(total / 1_000_000);
321
+ t.us = total - carrySeconds * 1_000_000;
322
+ const at = new Date(utcMs(t) + carrySeconds * 1000);
323
+ t.y = at.getUTCFullYear();
324
+ t.mo = at.getUTCMonth() + 1;
325
+ t.d = at.getUTCDate();
326
+ t.h = at.getUTCHours();
327
+ t.mi = at.getUTCMinutes();
328
+ t.s = at.getUTCSeconds();
329
+ }
330
+ /** The instant (ms) a DATETIME's wall time names in UTC, and the reverse; CONVERT_TZ's arithmetic. */
331
+ export const temporalUtcMs = (t) => utcMs(t);
332
+ export function temporalFromUtcMs(ms, us) {
333
+ const at = new Date(ms);
334
+ return { y: at.getUTCFullYear(), mo: at.getUTCMonth() + 1, d: at.getUTCDate(), h: at.getUTCHours(), mi: at.getUTCMinutes(), s: at.getUTCSeconds(), us, neg: false };
335
+ }
336
+ /** Round a value's fraction to `fsp` digits, as MySQL does on store (a half rounds up and carries). */
337
+ export function roundFsp(t, fsp, kind) {
338
+ const unit = 10 ** (6 - fsp);
339
+ const rounded = Math.round(t.us / unit) * unit;
340
+ const out = { ...t };
341
+ if (rounded === t.us)
342
+ return out;
343
+ if (kind === 'TIME') {
344
+ out.us = rounded;
345
+ if (out.us >= 1_000_000) {
346
+ out.us -= 1_000_000;
347
+ out.s += 1;
348
+ if (out.s === 60) {
349
+ out.s = 0;
350
+ out.mi += 1;
351
+ if (out.mi === 60) {
352
+ out.mi = 0;
353
+ out.h += 1;
354
+ }
355
+ }
356
+ }
357
+ return out;
358
+ }
359
+ if (t.y === 0 || t.mo === 0 || t.d === 0) {
360
+ out.us = Math.min(rounded, 999_999);
361
+ return out;
362
+ }
363
+ addMicros(out, rounded - t.us);
364
+ return out;
365
+ }
366
+ const p2 = (n) => String(n).padStart(2, '0');
367
+ /** The canonical text MySQL returns for a value of `kind` with `fsp` fractional digits. */
368
+ export function formatTemporal(t, kind, fsp) {
369
+ const frac = fsp > 0 ? `.${String(t.us).padStart(6, '0').slice(0, fsp)}` : '';
370
+ if (kind === 'TIME')
371
+ return `${t.neg && (t.h || t.mi || t.s || t.us) ? '-' : ''}${p2(t.h)}:${p2(t.mi)}:${p2(t.s)}${frac}`;
372
+ const date = `${String(t.y).padStart(4, '0')}-${p2(t.mo)}-${p2(t.d)}`;
373
+ if (kind === 'DATE')
374
+ return date;
375
+ return `${date} ${p2(t.h)}:${p2(t.mi)}:${p2(t.s)}${frac}`;
376
+ }
377
+ /** A totally ordered key of a temporal value: DATE/DATETIME/TIMESTAMP as their packed
378
+ * `YYYYMMDDhhmmss.ffffff`, a TIME as its signed microseconds. */
379
+ export function packTemporal(t, kind) {
380
+ if (kind === 'TIME') {
381
+ const v = ((BigInt(t.h) * 60n + BigInt(t.mi)) * 60n + BigInt(t.s)) * 1000000n + BigInt(t.us);
382
+ return t.neg ? -v : v;
383
+ }
384
+ const ymd = BigInt(t.y) * 10000n + BigInt(t.mo) * 100n + BigInt(t.d);
385
+ const hms = BigInt(t.h) * 10000n + BigInt(t.mi) * 100n + BigInt(t.s);
386
+ return (ymd * 1000000n + hms) * 1000000n + BigInt(t.us);
387
+ }
388
+ /** A temporal in numeric context: DATE → YYYYMMDD, DATETIME → YYYYMMDDhhmmss[.f], TIME → hhmmss[.f]. */
389
+ export function temporalNumberText(t, kind, fsp) {
390
+ const frac = fsp > 0 ? `.${String(t.us).padStart(6, '0').slice(0, fsp)}` : '';
391
+ if (kind === 'TIME')
392
+ return `${t.neg ? '-' : ''}${t.h}${p2(t.mi)}${p2(t.s)}${frac}`.replace(/^(-?)0+(?=\d)/, '$1');
393
+ const ymd = `${t.y}${p2(t.mo)}${p2(t.d)}`;
394
+ if (kind === 'DATE')
395
+ return String(Number(ymd));
396
+ return `${ymd}${p2(t.h)}${p2(t.mi)}${p2(t.s)}`.replace(/^0+(?=\d)/, '') + frac;
397
+ }
398
+ /** TIMESTAMP's range: '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999' UTC. */
399
+ export function inTimestampRange(t) {
400
+ const k = packTemporal(t, 'DATETIME');
401
+ return k >= packTemporal({ y: 1970, mo: 1, d: 1, h: 0, mi: 0, s: 1, us: 0, neg: false }, 'DATETIME')
402
+ && k <= packTemporal({ y: 2038, mo: 1, d: 19, h: 3, mi: 14, s: 7, us: 999_999, neg: false }, 'DATETIME');
403
+ }
404
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
405
+ // JSON
406
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
407
+ /** A JSON number. Integers are exact (MySQL keeps int64/uint64); a number with a fraction or an
408
+ * exponent, or beyond uint64, is a DOUBLE; a DECIMAL comes only from SQL (JSON_OBJECT('k', 1.50)). */
409
+ export class JNum {
410
+ kind;
411
+ int;
412
+ dbl;
413
+ dec;
414
+ constructor(kind, int, dbl, dec) {
415
+ this.kind = kind;
416
+ this.int = int;
417
+ this.dbl = dbl;
418
+ this.dec = dec;
419
+ }
420
+ static int(n) { return new JNum('int', n, Number(n), null); }
421
+ static double(n) { return new JNum('double', 0n, n, null); }
422
+ static decimal(d) { return new JNum('decimal', 0n, decToNumber(d), d); }
423
+ exact() { return this.kind === 'int' ? { m: this.int, s: 0 } : this.kind === 'decimal' ? this.dec : decFromNumber(this.dbl); }
424
+ }
425
+ /** A JSON object: members in MySQL's normalized order (shorter keys first, then byte order). */
426
+ export class JObj {
427
+ members;
428
+ constructor(members) {
429
+ this.members = members;
430
+ }
431
+ }
432
+ export class JsonTextError extends Error {
433
+ reason;
434
+ position;
435
+ constructor(reason, position) {
436
+ super(reason);
437
+ this.reason = reason;
438
+ this.position = position;
439
+ }
440
+ }
441
+ const INT64_MIN = -(2n ** 63n);
442
+ const UINT64_MAX = 2n ** 64n - 1n;
443
+ /** Parse JSON text (RFC 8259, what MySQL accepts). Throws `JsonTextError` with MySQL's reason. */
444
+ export function parseJson(text) {
445
+ let i = 0;
446
+ const ws = () => { while (i < text.length && ' \t\n\r'.includes(text[i]))
447
+ i++; };
448
+ const fail = (reason = 'Invalid value.') => { throw new JsonTextError(reason, i); };
449
+ const value = (depth) => {
450
+ if (depth > 100)
451
+ fail('The JSON document exceeds the maximum depth.');
452
+ ws();
453
+ const c = text[i];
454
+ if (c === undefined)
455
+ fail('The document is empty.');
456
+ if (c === '{') {
457
+ i++;
458
+ const members = new Map();
459
+ ws();
460
+ if (text[i] === '}') {
461
+ i++;
462
+ return obj(members);
463
+ }
464
+ for (;;) {
465
+ ws();
466
+ if (text[i] !== '"')
467
+ fail('Missing a name for object member.');
468
+ const k = str();
469
+ ws();
470
+ if (text[i] !== ':')
471
+ fail('Missing a colon after a name of object member.');
472
+ i++;
473
+ members.set(k, value(depth + 1));
474
+ ws();
475
+ if (text[i] === ',') {
476
+ i++;
477
+ continue;
478
+ }
479
+ if (text[i] === '}') {
480
+ i++;
481
+ return obj(members);
482
+ }
483
+ fail('Missing a comma or \'}\' after an object member.');
484
+ }
485
+ }
486
+ if (c === '[') {
487
+ i++;
488
+ const items = [];
489
+ ws();
490
+ if (text[i] === ']') {
491
+ i++;
492
+ return items;
493
+ }
494
+ for (;;) {
495
+ items.push(value(depth + 1));
496
+ ws();
497
+ if (text[i] === ',') {
498
+ i++;
499
+ continue;
500
+ }
501
+ if (text[i] === ']') {
502
+ i++;
503
+ return items;
504
+ }
505
+ fail('Missing a comma or \']\' after an array element.');
506
+ }
507
+ }
508
+ if (c === '"')
509
+ return str();
510
+ if (text.startsWith('true', i)) {
511
+ i += 4;
512
+ return true;
513
+ }
514
+ if (text.startsWith('false', i)) {
515
+ i += 5;
516
+ return false;
517
+ }
518
+ if (text.startsWith('null', i)) {
519
+ i += 4;
520
+ return null;
521
+ }
522
+ const m = /^-?(?:0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i));
523
+ if (m === null)
524
+ fail();
525
+ i += m[0].length;
526
+ return numberFromText(m[0], m[1] !== undefined || m[2] !== undefined);
527
+ };
528
+ const str = () => {
529
+ i++;
530
+ let out = '';
531
+ for (;;) {
532
+ const c = text[i];
533
+ if (c === undefined)
534
+ fail('Missing a closing quotation mark in string.');
535
+ if (c === '"') {
536
+ i++;
537
+ return out;
538
+ }
539
+ if (c === '\\') {
540
+ const e = text[i + 1];
541
+ const map = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
542
+ if (e !== undefined && map[e] !== undefined) {
543
+ out += map[e];
544
+ i += 2;
545
+ continue;
546
+ }
547
+ if (e === 'u' && /^[0-9a-fA-F]{4}$/.test(text.slice(i + 2, i + 6))) {
548
+ out += String.fromCharCode(parseInt(text.slice(i + 2, i + 6), 16));
549
+ i += 6;
550
+ continue;
551
+ }
552
+ fail('Invalid escape character in string.');
553
+ }
554
+ if (c.charCodeAt(0) < 0x20)
555
+ fail('Invalid encoding in string.');
556
+ out += c;
557
+ i++;
558
+ }
559
+ };
560
+ const result = value(0);
561
+ ws();
562
+ if (i < text.length)
563
+ fail('The document root must not be followed by other values.');
564
+ return result;
565
+ }
566
+ function obj(members) {
567
+ const sorted = [...members.keys()].sort(compareKeys);
568
+ return new JObj(new Map(sorted.map((k) => [k, members.get(k)])));
569
+ }
570
+ const utf8Len = (s) => new TextEncoder().encode(s).length;
571
+ function compareKeys(a, b) {
572
+ const la = utf8Len(a);
573
+ const lb = utf8Len(b);
574
+ if (la !== lb)
575
+ return la - lb;
576
+ return compareUtf8(a, b);
577
+ }
578
+ function compareUtf8(a, b) {
579
+ const x = new TextEncoder().encode(a);
580
+ const y = new TextEncoder().encode(b);
581
+ for (let k = 0; k < Math.min(x.length, y.length); k++)
582
+ if (x[k] !== y[k])
583
+ return x[k] - y[k];
584
+ return x.length - y.length;
585
+ }
586
+ /** A JSON object built from members in any order (JSON_OBJECT), normalized as MySQL stores it. */
587
+ export const jsonObject = (members) => obj(members);
588
+ function numberFromText(text, fractional) {
589
+ if (!fractional) {
590
+ const n = BigInt(text);
591
+ if (n >= INT64_MIN && n <= UINT64_MAX)
592
+ return JNum.int(n);
593
+ }
594
+ return JNum.double(Number(text));
595
+ }
596
+ /** A JSON value printed as MySQL prints one: `", "` and `": "` separators, normalized member order,
597
+ * doubles with a `.0` when they would otherwise read as integers. */
598
+ export function printJson(v) {
599
+ if (v === null)
600
+ return 'null';
601
+ if (v === true)
602
+ return 'true';
603
+ if (v === false)
604
+ return 'false';
605
+ if (typeof v === 'string')
606
+ return JSON.stringify(v);
607
+ if (v instanceof JNum) {
608
+ if (v.kind === 'int')
609
+ return v.int.toString();
610
+ if (v.kind === 'decimal')
611
+ return decText(v.dec);
612
+ const text = formatDouble(v.dbl);
613
+ return /^-?\d+$/.test(text) ? `${text}.0` : text;
614
+ }
615
+ if (Array.isArray(v))
616
+ return `[${v.map(printJson).join(', ')}]`;
617
+ return `{${[...v.members].map(([k, x]) => `${JSON.stringify(k)}: ${printJson(x)}`).join(', ')}}`;
618
+ }
619
+ export function jsonTypeName(v) {
620
+ if (v === null)
621
+ return 'NULL';
622
+ if (typeof v === 'boolean')
623
+ return 'BOOLEAN';
624
+ if (typeof v === 'string')
625
+ return 'STRING';
626
+ if (v instanceof JNum)
627
+ return v.kind === 'int' ? (v.int > 2n ** 63n - 1n ? 'UNSIGNED INTEGER' : 'INTEGER') : v.kind === 'decimal' ? 'DECIMAL' : 'DOUBLE';
628
+ return Array.isArray(v) ? 'ARRAY' : 'OBJECT';
629
+ }
630
+ /** MySQL's JSON type precedence for ordering values of different types (higher sorts later):
631
+ * BOOLEAN > ARRAY > OBJECT > STRING > numbers > NULL (the BLOB/BIT/OPAQUE/temporal types above
632
+ * BOOLEAN are never produced by parsing JSON text). */
633
+ function precedence(v) {
634
+ if (v === null)
635
+ return 0;
636
+ if (v instanceof JNum)
637
+ return 1;
638
+ if (typeof v === 'string')
639
+ return 2;
640
+ if (v instanceof JObj)
641
+ return 3;
642
+ if (Array.isArray(v))
643
+ return 4;
644
+ return 5;
645
+ }
646
+ /** Compare two JSON values (manual 13.5, "Comparison and Ordering of JSON Values"). Values of
647
+ * different types order by type precedence; numbers compare by value whatever their kind; strings
648
+ * by their utf8mb4 bytes; arrays element by element, the shorter first on a tie; booleans false
649
+ * first; objects are equal when they have the same members, otherwise ordered deterministically. */
650
+ export function compareJson(a, b) {
651
+ const pa = precedence(a);
652
+ const pb = precedence(b);
653
+ if (pa !== pb)
654
+ return pa - pb;
655
+ if (a === null)
656
+ return 0;
657
+ if (a instanceof JNum) {
658
+ const x = a;
659
+ const y = b;
660
+ if (x.kind === 'double' && y.kind === 'double')
661
+ return x.dbl < y.dbl ? -1 : x.dbl > y.dbl ? 1 : 0;
662
+ return decCmp(x.exact(), y.exact());
663
+ }
664
+ if (typeof a === 'string')
665
+ return compareUtf8(a, b);
666
+ if (typeof a === 'boolean')
667
+ return (a ? 1 : 0) - (b ? 1 : 0);
668
+ if (Array.isArray(a)) {
669
+ const y = b;
670
+ for (let k = 0; k < Math.min(a.length, y.length); k++) {
671
+ const c = compareJson(a[k], y[k]);
672
+ if (c !== 0)
673
+ return c;
674
+ }
675
+ return a.length - y.length;
676
+ }
677
+ const x = a.members;
678
+ const y = b.members;
679
+ if (x.size === y.size && [...x].every(([k, v]) => y.has(k) && compareJson(v, y.get(k)) === 0))
680
+ return 0;
681
+ const tx = printJson(a);
682
+ const ty = printJson(b);
683
+ return tx < ty ? -1 : tx > ty ? 1 : 0;
684
+ }
685
+ /** A key under which JSON values that compare equal meet (numbers by value, objects by members). */
686
+ export function jsonKey(v) {
687
+ if (v instanceof JNum)
688
+ return `n${decKey(v.exact())}`;
689
+ if (Array.isArray(v))
690
+ return `a[${v.map(jsonKey).join(',')}]`;
691
+ if (v instanceof JObj)
692
+ return `o{${[...v.members].map(([k, x]) => `${JSON.stringify(k)}:${jsonKey(x)}`).join(',')}}`;
693
+ return `v${JSON.stringify(v)}`;
694
+ }
695
+ /** JSON_CONTAINS (manual 14.17.3): a scalar contains an equal scalar; an array contains every
696
+ * element of a candidate array, or a candidate non-array equal to one of its elements; an object
697
+ * contains a candidate object whose every member it contains. */
698
+ export function jsonContains(target, candidate) {
699
+ if (Array.isArray(target)) {
700
+ if (Array.isArray(candidate))
701
+ return candidate.every((c) => target.some((t) => jsonContains(t, c)));
702
+ return target.some((t) => jsonContains(t, candidate));
703
+ }
704
+ if (target instanceof JObj) {
705
+ if (!(candidate instanceof JObj))
706
+ return false;
707
+ return [...candidate.members].every(([k, v]) => target.members.has(k) && jsonContains(target.members.get(k), v));
708
+ }
709
+ if (Array.isArray(candidate) || candidate instanceof JObj)
710
+ return false;
711
+ return compareJson(target, candidate) === 0;
712
+ }
713
+ export function jsonDepth(v) {
714
+ if (Array.isArray(v))
715
+ return 1 + Math.max(0, ...v.map(jsonDepth));
716
+ if (v instanceof JObj)
717
+ return 1 + Math.max(0, ...[...v.members.values()].map(jsonDepth));
718
+ return 1;
719
+ }