@mason-data/cli 0.0.0-stage → 0.7.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/LICENSE +202 -0
- package/README.md +268 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +477 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1771 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +588 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +198 -0
- package/dist/src/cli/command.js +173 -0
- package/dist/src/cli/commands.js +1277 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +669 -0
- package/dist/src/cli/doctor.js +260 -0
- package/dist/src/cli/guides.js +158 -0
- package/dist/src/cli/init.js +366 -0
- package/dist/src/cli/machine.js +162 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +347 -0
- package/dist/src/cli/project.js +129 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +220 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +311 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +301 -0
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +564 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +268 -0
- package/dist/src/duck/executor.js +567 -0
- package/dist/src/duck/explain.js +238 -0
- package/dist/src/duck/functions.js +819 -0
- package/dist/src/duck/gaps.js +94 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +188 -0
- package/dist/src/duck/kept-route.js +299 -0
- package/dist/src/duck/kept.js +964 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +378 -0
- package/dist/src/duck/plan.js +259 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +5101 -0
- package/dist/src/duck/store.js +907 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2929 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +1001 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +237 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +76 -0
- package/dist/src/web/app.js +491 -0
- package/dist/src/web/index.html +44 -0
- package/package.json +22 -4
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSTGRES'S to_char (formatting.c), in JavaScript: what the rewrite calls for `to_char(time, fmt)` and
|
|
3
|
+
* `to_char(number, fmt)` (src/duck/functions.ts registers it), because DuckDB's strftime has neither Postgres's
|
|
4
|
+
* patterns (Q, IYYY, FMMonth, a name padded to nine) nor its number templates (`FM999,999,990.00`). Held to Postgres
|
|
5
|
+
* itself by tests/integration/duck-to-char.itest.ts, which formats the same values both ways.
|
|
6
|
+
*
|
|
7
|
+
* English names only (Postgres's TM prefix translates, and is read as English here); a pattern this does not write
|
|
8
|
+
* (TZ's abbreviation, SP, EEEE, RN, V) raises an error naming it rather than writing something else.
|
|
9
|
+
*/
|
|
10
|
+
export class ToCharError extends Error {
|
|
11
|
+
}
|
|
12
|
+
// ------------------------------------------------------------------------------------------------- times
|
|
13
|
+
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
14
|
+
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
15
|
+
const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
|
|
16
|
+
function readWall(text) {
|
|
17
|
+
if (/infinity/i.test(text))
|
|
18
|
+
return null;
|
|
19
|
+
const m = /^(-?\d+)-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?)?(?:([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?( \(BC\))?$/.exec(text.trim());
|
|
20
|
+
if (m === null)
|
|
21
|
+
throw new ToCharError(`to_char cannot read the time ${JSON.stringify(text)}: report it (Mason)`);
|
|
22
|
+
const bc = m[12] !== undefined;
|
|
23
|
+
const year = Number(m[1]);
|
|
24
|
+
return {
|
|
25
|
+
year: bc ? 1 - year : year, month: Number(m[2]), day: Number(m[3]),
|
|
26
|
+
hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0), second: Number(m[6] ?? 0), micro: Number((m[7] ?? "").padEnd(6, "0")),
|
|
27
|
+
offset: m[8] === undefined ? null : (m[8] === "-" ? -1 : 1) * (Number(m[9]) * 3600 + Number(m[10] ?? 0) * 60 + Number(m[11] ?? 0)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** days from 1970-01-01 of a proleptic Gregorian date */
|
|
31
|
+
function epochDay(y, m, d) {
|
|
32
|
+
const a = Math.floor((14 - m) / 12), yy = y + 4800 - a, mm = m + 12 * a - 3;
|
|
33
|
+
return d + Math.floor((153 * mm + 2) / 5) + 365 * yy + Math.floor(yy / 4) - Math.floor(yy / 100) + Math.floor(yy / 400) - 32045 - 2440588;
|
|
34
|
+
}
|
|
35
|
+
const dayOfWeek = (y, m, d) => ((epochDay(y, m, d) % 7) + 7 + 4) % 7; // 0 = Sunday
|
|
36
|
+
const dayOfYear = (y, m, d) => epochDay(y, m, d) - epochDay(y, 1, 1) + 1;
|
|
37
|
+
/** the ISO 8601 year and week of a date, and its day of the ISO year */
|
|
38
|
+
function isoWeek(y, m, d) {
|
|
39
|
+
const day = epochDay(y, m, d);
|
|
40
|
+
const isoDow = ((dayOfWeek(y, m, d) + 6) % 7) + 1; // 1 = Monday
|
|
41
|
+
const thursday = day - isoDow + 4;
|
|
42
|
+
const thursdayYear = yearOf(thursday);
|
|
43
|
+
const week1Monday = (yy) => { const jan4 = epochDay(yy, 1, 4); const dow = ((dayOfWeekOf(jan4) + 6) % 7) + 1; return jan4 - dow + 1; };
|
|
44
|
+
const start = week1Monday(thursdayYear);
|
|
45
|
+
return { year: thursdayYear, week: Math.floor((day - start) / 7) + 1, yday: day - start + 1 };
|
|
46
|
+
}
|
|
47
|
+
const dayOfWeekOf = (epoch) => ((epoch % 7) + 7 + 4) % 7;
|
|
48
|
+
function yearOf(epoch) {
|
|
49
|
+
let y = Math.floor(epoch / 365.2425) + 1970;
|
|
50
|
+
while (epochDay(y, 1, 1) > epoch)
|
|
51
|
+
y--;
|
|
52
|
+
while (epochDay(y + 1, 1, 1) <= epoch)
|
|
53
|
+
y++;
|
|
54
|
+
return y;
|
|
55
|
+
}
|
|
56
|
+
/** the template's patterns, longest first where one is another's prefix (formatting.c's DCH_keywords) */
|
|
57
|
+
const TIME_PATTERNS = [
|
|
58
|
+
"A.D.", "A.M.", "AD", "AM", "B.C.", "BC", "CC", "DAY", "DDD", "DD", "DY", "Day", "Dy", "D", "FF1", "FF2", "FF3", "FF4", "FF5", "FF6",
|
|
59
|
+
"HH24", "HH12", "HH", "IDDD", "ID", "IW", "IYYY", "IYY", "IY", "I", "J", "MI", "MM", "MONTH", "MON", "MS", "Month", "Mon", "OF",
|
|
60
|
+
"P.M.", "PM", "Q", "RM", "SSSSS", "SSSS", "SS", "TZH", "TZM", "TZ", "US", "WW", "W", "Y,YYY", "YYYY", "YYY", "YY", "Y",
|
|
61
|
+
"a.d.", "a.m.", "ad", "am", "b.c.", "bc", "cc", "day", "ddd", "dd", "dy", "d", "ff1", "ff2", "ff3", "ff4", "ff5", "ff6",
|
|
62
|
+
"hh24", "hh12", "hh", "iddd", "id", "iw", "iyyy", "iyy", "iy", "i", "j", "mi", "mm", "month", "mon", "ms", "of",
|
|
63
|
+
"p.m.", "pm", "q", "rm", "sssss", "ssss", "ss", "tzh", "tzm", "tz", "us", "ww", "w", "y,yyy", "yyyy", "yyy", "yy", "y",
|
|
64
|
+
];
|
|
65
|
+
/** an ordinal's suffix (TH / th) */
|
|
66
|
+
function ordinal(n, upper) {
|
|
67
|
+
const t = n % 100, u = n % 10;
|
|
68
|
+
const s = t >= 11 && t <= 13 ? "th" : u === 1 ? "st" : u === 2 ? "nd" : u === 3 ? "rd" : "th";
|
|
69
|
+
return upper ? s.toUpperCase() : s;
|
|
70
|
+
}
|
|
71
|
+
/** a name in the pattern's case (MONTH, Month, month), padded to nine unless FM */
|
|
72
|
+
function named(name, pattern, fm, width) {
|
|
73
|
+
const cased = pattern === pattern.toUpperCase() ? name.toUpperCase() : pattern[0] === pattern[0].toUpperCase() ? name : name.toLowerCase();
|
|
74
|
+
return fm ? cased : cased.padEnd(width, " ");
|
|
75
|
+
}
|
|
76
|
+
/** to_char(timestamp | timestamptz | date, fmt): `text` is DuckDB's text of the value (in the session's zone) */
|
|
77
|
+
export function toCharTime(text, fmt) {
|
|
78
|
+
const w = readWall(text);
|
|
79
|
+
if (w === null)
|
|
80
|
+
return null;
|
|
81
|
+
const { year, month, day, hour, minute, second, micro } = w;
|
|
82
|
+
let out = "", i = 0;
|
|
83
|
+
while (i < fmt.length) {
|
|
84
|
+
// FM (fill mode) and TM (translation: English here) prefix the next pattern
|
|
85
|
+
let fm = false;
|
|
86
|
+
for (;;) {
|
|
87
|
+
if (fmt.startsWith("FM", i) || fmt.startsWith("fm", i)) {
|
|
88
|
+
fm = true;
|
|
89
|
+
i += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// TM translates the names (English here) and, as Postgres does, does not pad them
|
|
93
|
+
if (fmt.startsWith("TM", i) || fmt.startsWith("tm", i)) {
|
|
94
|
+
fm = true;
|
|
95
|
+
i += 2;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (fmt.startsWith("FX", i) || fmt.startsWith("fx", i)) {
|
|
99
|
+
i += 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (i >= fmt.length)
|
|
105
|
+
break;
|
|
106
|
+
const c = fmt[i];
|
|
107
|
+
if (c === '"') {
|
|
108
|
+
// a quoted literal; a backslash escapes the next character in it
|
|
109
|
+
i++;
|
|
110
|
+
while (i < fmt.length && fmt[i] !== '"') {
|
|
111
|
+
if (fmt[i] === "\\" && i + 1 < fmt.length)
|
|
112
|
+
i++;
|
|
113
|
+
out += fmt[i];
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (c === "\\" && fmt[i + 1] === '"') {
|
|
120
|
+
out += '"';
|
|
121
|
+
i += 2;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const pattern = TIME_PATTERNS.find((p) => fmt.startsWith(p, i));
|
|
125
|
+
if (pattern === undefined) {
|
|
126
|
+
out += c;
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
i += pattern.length;
|
|
131
|
+
const th = fmt.startsWith("TH", i) ? "TH" : fmt.startsWith("th", i) ? "th" : null;
|
|
132
|
+
if (th !== null)
|
|
133
|
+
i += 2;
|
|
134
|
+
if (fmt.startsWith("SP", i) || fmt.startsWith("sp", i))
|
|
135
|
+
throw new ToCharError(`the to_char suffix SP is not supported`);
|
|
136
|
+
const num = (n, width) => {
|
|
137
|
+
const neg = n < 0;
|
|
138
|
+
const digits = String(Math.abs(n));
|
|
139
|
+
const body = fm ? digits : digits.padStart(width, "0");
|
|
140
|
+
return (neg ? "-" : "") + body + (th === null ? "" : ordinal(Math.abs(n), th === "TH"));
|
|
141
|
+
};
|
|
142
|
+
const P = pattern.toUpperCase();
|
|
143
|
+
const h12 = hour % 12 === 0 ? 12 : hour % 12;
|
|
144
|
+
const pm = hour >= 12;
|
|
145
|
+
const iso = () => isoWeek(year, month, day);
|
|
146
|
+
switch (P) {
|
|
147
|
+
case "HH":
|
|
148
|
+
case "HH12":
|
|
149
|
+
out += num(h12, 2);
|
|
150
|
+
break;
|
|
151
|
+
case "HH24":
|
|
152
|
+
out += num(hour, 2);
|
|
153
|
+
break;
|
|
154
|
+
case "MI":
|
|
155
|
+
out += num(minute, 2);
|
|
156
|
+
break;
|
|
157
|
+
case "SS":
|
|
158
|
+
out += num(second, 2);
|
|
159
|
+
break;
|
|
160
|
+
case "MS":
|
|
161
|
+
out += num(Math.floor(micro / 1000), 3);
|
|
162
|
+
break;
|
|
163
|
+
case "US":
|
|
164
|
+
out += num(micro, 6);
|
|
165
|
+
break;
|
|
166
|
+
case "FF1":
|
|
167
|
+
case "FF2":
|
|
168
|
+
case "FF3":
|
|
169
|
+
case "FF4":
|
|
170
|
+
case "FF5":
|
|
171
|
+
case "FF6": {
|
|
172
|
+
const n = Number(P[2]);
|
|
173
|
+
out += num(Math.floor(micro / 10 ** (6 - n)), n);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "SSSS":
|
|
177
|
+
case "SSSSS":
|
|
178
|
+
out += num(hour * 3600 + minute * 60 + second, 0);
|
|
179
|
+
break;
|
|
180
|
+
case "AM":
|
|
181
|
+
case "PM":
|
|
182
|
+
out += pattern === P ? (pm ? "PM" : "AM") : (pm ? "pm" : "am");
|
|
183
|
+
break;
|
|
184
|
+
case "A.M.":
|
|
185
|
+
case "P.M.":
|
|
186
|
+
out += pattern === P ? (pm ? "P.M." : "A.M.") : (pm ? "p.m." : "a.m.");
|
|
187
|
+
break;
|
|
188
|
+
case "AD":
|
|
189
|
+
case "BC":
|
|
190
|
+
out += pattern === P ? (year <= 0 ? "BC" : "AD") : (year <= 0 ? "bc" : "ad");
|
|
191
|
+
break;
|
|
192
|
+
case "A.D.":
|
|
193
|
+
case "B.C.":
|
|
194
|
+
out += pattern === P ? (year <= 0 ? "B.C." : "A.D.") : (year <= 0 ? "b.c." : "a.d.");
|
|
195
|
+
break;
|
|
196
|
+
case "Y,YYY": {
|
|
197
|
+
const y = year <= 0 ? 1 - year : year;
|
|
198
|
+
out += `${Math.floor(y / 1000)},${String(y % 1000).padStart(3, "0")}`;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "YYYY":
|
|
202
|
+
out += num(year <= 0 ? 1 - year : year, 4);
|
|
203
|
+
break;
|
|
204
|
+
case "YYY":
|
|
205
|
+
out += num((year <= 0 ? 1 - year : year) % 1000, 3);
|
|
206
|
+
break;
|
|
207
|
+
case "YY":
|
|
208
|
+
out += num((year <= 0 ? 1 - year : year) % 100, 2);
|
|
209
|
+
break;
|
|
210
|
+
case "Y":
|
|
211
|
+
out += num((year <= 0 ? 1 - year : year) % 10, 1);
|
|
212
|
+
break;
|
|
213
|
+
case "IYYY":
|
|
214
|
+
out += num(iso().year, 4);
|
|
215
|
+
break;
|
|
216
|
+
case "IYY":
|
|
217
|
+
out += num(iso().year % 1000, 3);
|
|
218
|
+
break;
|
|
219
|
+
case "IY":
|
|
220
|
+
out += num(iso().year % 100, 2);
|
|
221
|
+
break;
|
|
222
|
+
case "I":
|
|
223
|
+
out += num(iso().year % 10, 1);
|
|
224
|
+
break;
|
|
225
|
+
case "MONTH":
|
|
226
|
+
out += named(MONTHS[month - 1], pattern, fm, 9);
|
|
227
|
+
break;
|
|
228
|
+
case "MON":
|
|
229
|
+
out += named(MONTHS[month - 1].slice(0, 3), pattern, true, 3);
|
|
230
|
+
break;
|
|
231
|
+
case "MM":
|
|
232
|
+
out += num(month, 2);
|
|
233
|
+
break;
|
|
234
|
+
case "DAY":
|
|
235
|
+
out += named(DAYS[dayOfWeek(year, month, day)], pattern, fm, 9);
|
|
236
|
+
break;
|
|
237
|
+
case "DY":
|
|
238
|
+
out += named(DAYS[dayOfWeek(year, month, day)].slice(0, 3), pattern, true, 3);
|
|
239
|
+
break;
|
|
240
|
+
case "DDD":
|
|
241
|
+
out += num(dayOfYear(year, month, day), 3);
|
|
242
|
+
break;
|
|
243
|
+
case "IDDD":
|
|
244
|
+
out += num(iso().yday, 3);
|
|
245
|
+
break;
|
|
246
|
+
case "DD":
|
|
247
|
+
out += num(day, 2);
|
|
248
|
+
break;
|
|
249
|
+
case "D":
|
|
250
|
+
out += num(dayOfWeek(year, month, day) + 1, 1);
|
|
251
|
+
break;
|
|
252
|
+
case "ID":
|
|
253
|
+
out += num(((dayOfWeek(year, month, day) + 6) % 7) + 1, 1);
|
|
254
|
+
break;
|
|
255
|
+
case "W":
|
|
256
|
+
out += num(Math.floor((day - 1) / 7) + 1, 1);
|
|
257
|
+
break;
|
|
258
|
+
case "WW":
|
|
259
|
+
out += num(Math.floor((dayOfYear(year, month, day) - 1) / 7) + 1, 2);
|
|
260
|
+
break;
|
|
261
|
+
case "IW":
|
|
262
|
+
out += num(iso().week, 2);
|
|
263
|
+
break;
|
|
264
|
+
case "CC": {
|
|
265
|
+
const cc = year > 0 ? Math.floor((year + 99) / 100) : -Math.floor((99 - year) / 100);
|
|
266
|
+
out += num(cc, 2);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "J":
|
|
270
|
+
out += num(epochDay(year, month, day) + 2440588, 0);
|
|
271
|
+
break;
|
|
272
|
+
case "Q":
|
|
273
|
+
out += num(Math.floor((month - 1) / 3) + 1, 0);
|
|
274
|
+
break;
|
|
275
|
+
case "RM": {
|
|
276
|
+
const r = ROMAN[month - 1];
|
|
277
|
+
const cased = pattern === P ? r : r.toLowerCase();
|
|
278
|
+
out += fm ? cased : cased.padEnd(4, " ");
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "TZH":
|
|
282
|
+
case "TZM":
|
|
283
|
+
case "OF": {
|
|
284
|
+
if (w.offset === null)
|
|
285
|
+
break;
|
|
286
|
+
const sign = w.offset < 0 ? "-" : "+", a = Math.abs(w.offset);
|
|
287
|
+
const hh = String(Math.floor(a / 3600)).padStart(2, "0"), mm = String(Math.floor((a % 3600) / 60)).padStart(2, "0");
|
|
288
|
+
out += P === "TZH" ? `${sign}${hh}` : P === "TZM" ? mm : `${sign}${hh}${mm === "00" ? "" : `:${mm}`}`;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "TZ": throw new ToCharError("the to_char pattern TZ (the zone's abbreviation) is not supported: use OF for the offset");
|
|
292
|
+
default: out += pattern;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
// ------------------------------------------------------------------------------------------------- numbers
|
|
298
|
+
/** a decimal text rounded half away from zero to `scale` decimals: its sign, integer digits and fraction digits */
|
|
299
|
+
function roundDecimal(text, scale) {
|
|
300
|
+
const m = /^\s*([+-])?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?\s*$/.exec(text);
|
|
301
|
+
if (m === null) {
|
|
302
|
+
if (/^\s*-?inf/i.test(text) || /^\s*nan/i.test(text))
|
|
303
|
+
throw new ToCharError(`to_char of ${text.trim()} is not supported`);
|
|
304
|
+
throw new ToCharError(`to_char cannot read the number ${JSON.stringify(text)}: report it (Mason)`);
|
|
305
|
+
}
|
|
306
|
+
let digits = `${m[2] ?? ""}${m[3] ?? ""}`;
|
|
307
|
+
let point = (m[2] ?? "").length + Number(m[4] ?? 0);
|
|
308
|
+
if (point < 0) {
|
|
309
|
+
digits = "0".repeat(-point) + digits;
|
|
310
|
+
point = 0;
|
|
311
|
+
}
|
|
312
|
+
if (point > digits.length)
|
|
313
|
+
digits = digits.padEnd(point, "0");
|
|
314
|
+
const keep = point + scale;
|
|
315
|
+
let kept = digits.slice(0, Math.max(0, keep)).padEnd(Math.max(0, keep), "0");
|
|
316
|
+
if (keep >= 0 && Number(digits[keep] ?? "0") >= 5) {
|
|
317
|
+
// carry the rounding up through the kept digits
|
|
318
|
+
const arr = kept.split("");
|
|
319
|
+
let k = arr.length - 1;
|
|
320
|
+
for (; k >= 0; k--) {
|
|
321
|
+
if (arr[k] === "9")
|
|
322
|
+
arr[k] = "0";
|
|
323
|
+
else {
|
|
324
|
+
arr[k] = String(Number(arr[k]) + 1);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
kept = (k < 0 ? "1" : "") + arr.join("");
|
|
329
|
+
if (k < 0)
|
|
330
|
+
point++;
|
|
331
|
+
}
|
|
332
|
+
const int = kept.slice(0, kept.length - scale).replace(/^0+/, "");
|
|
333
|
+
const frac = scale > 0 ? kept.slice(kept.length - scale) : "";
|
|
334
|
+
const neg = m[1] === "-" && /[1-9]/.test(int + frac);
|
|
335
|
+
return { neg, int, frac };
|
|
336
|
+
}
|
|
337
|
+
/** to_char(number, fmt): `text` is the number's decimal text */
|
|
338
|
+
export function toCharNumber(text, fmt) {
|
|
339
|
+
const tokens = [];
|
|
340
|
+
let fm = false, i = 0;
|
|
341
|
+
while (i < fmt.length) {
|
|
342
|
+
const rest = fmt.slice(i);
|
|
343
|
+
const word = ["FM", "fm", "MI", "mi", "PL", "pl", "SG", "sg", "PR", "pr", "EEEE", "eeee", "RN", "rn", "TH", "th", "V", "v", "S", "s", "D", "d", "G", "g", "B", "b", "L", "l"].find((w) => rest.startsWith(w));
|
|
344
|
+
if (word === "FM" || word === "fm") {
|
|
345
|
+
fm = true;
|
|
346
|
+
i += 2;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const W = word?.toUpperCase();
|
|
350
|
+
if (W === "EEEE" || W === "RN" || W === "TH" || W === "V" || W === "B" || W === "L")
|
|
351
|
+
throw new ToCharError(`the to_char number pattern ${W} is not supported`);
|
|
352
|
+
if (W === "MI" || W === "PL" || W === "SG" || W === "PR" || W === "S") {
|
|
353
|
+
tokens.push({ t: W, text: W });
|
|
354
|
+
i += W.length;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (W === "D") {
|
|
358
|
+
tokens.push({ t: ".", text: "." });
|
|
359
|
+
i++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (W === "G") {
|
|
363
|
+
tokens.push({ t: ",", text: "," });
|
|
364
|
+
i++;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const c = fmt[i];
|
|
368
|
+
if (c === "9" || c === "0" || c === "." || c === ",") {
|
|
369
|
+
tokens.push({ t: c, text: c });
|
|
370
|
+
i++;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (c === '"') {
|
|
374
|
+
i++;
|
|
375
|
+
let lit = "";
|
|
376
|
+
while (i < fmt.length && fmt[i] !== '"') {
|
|
377
|
+
if (fmt[i] === "\\")
|
|
378
|
+
i++;
|
|
379
|
+
lit += fmt[i] ?? "";
|
|
380
|
+
i++;
|
|
381
|
+
}
|
|
382
|
+
i++;
|
|
383
|
+
tokens.push({ t: "lit", text: lit });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
tokens.push({ t: "lit", text: c });
|
|
387
|
+
i++;
|
|
388
|
+
}
|
|
389
|
+
const pointAt = tokens.findIndex((x) => x.t === ".");
|
|
390
|
+
const intTokens = pointAt < 0 ? tokens : tokens.slice(0, pointAt);
|
|
391
|
+
const fracTokens = pointAt < 0 ? [] : tokens.slice(pointAt + 1);
|
|
392
|
+
const intDigits = intTokens.filter((x) => x.t === "9" || x.t === "0").length;
|
|
393
|
+
const fracDigits = fracTokens.filter((x) => x.t === "9" || x.t === "0").length;
|
|
394
|
+
const { neg, int, frac } = roundDecimal(text, fracDigits);
|
|
395
|
+
// the first `0` of the integer part prints every digit place after it, zero or not
|
|
396
|
+
const firstZero = intTokens.findIndex((x) => x.t === "0");
|
|
397
|
+
const digitPlaces = intTokens.map((x, k) => x.t === "9" || x.t === "0" ? k : -1).filter((k) => k >= 0);
|
|
398
|
+
const zeroFrom = firstZero < 0 ? Infinity : digitPlaces.indexOf(firstZero);
|
|
399
|
+
// a value with no integer digits shows its 0 only in the last place, and only without a fraction place (Postgres:
|
|
400
|
+
// to_char(0, '999') is '0', to_char(0.5, '9.99') is ' .50')
|
|
401
|
+
const intText = int === "" && pointAt < 0 ? "0" : int;
|
|
402
|
+
const overflow = intText.length > intDigits;
|
|
403
|
+
const padded = intText.padStart(intDigits, " ");
|
|
404
|
+
const firstDigit = tokens.findIndex((x) => x.t === "9" || x.t === "0");
|
|
405
|
+
const sTrailing = tokens.some((x, k) => x.t === "S" && k > firstDigit && firstDigit >= 0);
|
|
406
|
+
const has = (t) => tokens.some((x) => x.t === t);
|
|
407
|
+
// the fixed-place signs (SG, MI, PL, a trailing S) are written where the template has them; a placeholder until then
|
|
408
|
+
const fixed = (t) => {
|
|
409
|
+
switch (t) {
|
|
410
|
+
case "SG": return neg ? "-" : "+";
|
|
411
|
+
case "MI": return neg ? "-" : fm ? "" : " ";
|
|
412
|
+
case "PL": return neg ? (fm ? "" : " ") : "+";
|
|
413
|
+
case "S": return sTrailing ? (neg ? "-" : "+") : "";
|
|
414
|
+
default: return "";
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
let body = "", place = 0, started = false;
|
|
418
|
+
for (const x of intTokens) {
|
|
419
|
+
if (x.t === "9" || x.t === "0") {
|
|
420
|
+
let ch = overflow ? "#" : padded[place];
|
|
421
|
+
if (ch === " " && place >= zeroFrom)
|
|
422
|
+
ch = "0";
|
|
423
|
+
if (ch !== " ")
|
|
424
|
+
started = true;
|
|
425
|
+
// fill mode writes no padding at all (a blank a digit place would have held)
|
|
426
|
+
if (!(fm && ch === " "))
|
|
427
|
+
body += ch;
|
|
428
|
+
place++;
|
|
429
|
+
}
|
|
430
|
+
else if (x.t === ",")
|
|
431
|
+
body += started ? "," : fm ? "" : " ";
|
|
432
|
+
else if (x.t === "lit")
|
|
433
|
+
body += x.text;
|
|
434
|
+
else
|
|
435
|
+
body += fixed(x.t);
|
|
436
|
+
}
|
|
437
|
+
if (pointAt >= 0) {
|
|
438
|
+
let fraction = "", fp = 0;
|
|
439
|
+
const places = [];
|
|
440
|
+
for (const x of fracTokens) {
|
|
441
|
+
if (x.t === "9" || x.t === "0") {
|
|
442
|
+
fraction += overflow ? "#" : frac[fp] ?? "0";
|
|
443
|
+
places.push(x.t);
|
|
444
|
+
fp++;
|
|
445
|
+
}
|
|
446
|
+
else if (x.t === ",")
|
|
447
|
+
fraction += ",";
|
|
448
|
+
else if (x.t === "lit")
|
|
449
|
+
fraction += x.text;
|
|
450
|
+
else
|
|
451
|
+
fraction += `\u0000${fixed(x.t)}\u0000`;
|
|
452
|
+
}
|
|
453
|
+
if (fm && !overflow) {
|
|
454
|
+
// fill mode drops the trailing zeros of the 9 places after the point
|
|
455
|
+
let cut = 0;
|
|
456
|
+
for (let k = places.length - 1; k >= 0 && places[k] === "9" && frac[k] === "0"; k--)
|
|
457
|
+
cut++;
|
|
458
|
+
for (let removed = 0, k = fraction.length - 1; removed < cut && k >= 0; k--) {
|
|
459
|
+
if (/\d/.test(fraction[k])) {
|
|
460
|
+
fraction = fraction.slice(0, k) + fraction.slice(k + 1);
|
|
461
|
+
removed++;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// a value with no digit left is written 0 before the point (to_char(0, 'FM9.99') is `0.`)
|
|
465
|
+
if (!/\d/.test(body + fraction))
|
|
466
|
+
body = `${body.replace(/ +$/, "")}0`;
|
|
467
|
+
}
|
|
468
|
+
body += `.${fraction.replace(/\u0000/g, "")}`;
|
|
469
|
+
}
|
|
470
|
+
// the floating sign: just before the number's first digit, point or # (after the padding blanks and any text before
|
|
471
|
+
// it) — the default's (a blank for a positive number), a leading S's (+ or -), PR's < (a blank for a positive number)
|
|
472
|
+
const pr = has("PR");
|
|
473
|
+
const floating = pr ? (neg ? "<" : fm ? "" : " ")
|
|
474
|
+
: has("S") && !sTrailing ? (neg ? "-" : "+")
|
|
475
|
+
: !has("S") && !has("SG") && !has("MI") ? (neg ? "-" : fm ? "" : " ")
|
|
476
|
+
: "";
|
|
477
|
+
const lead = Math.max(0, body.search(/[0-9#.]/));
|
|
478
|
+
let out = `${body.slice(0, lead)}${floating}${body.slice(lead)}`;
|
|
479
|
+
if (pr)
|
|
480
|
+
out += neg ? ">" : fm ? "" : " ";
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROTOTYPE'S TOKEN (decision 5 of the prototype's design): `mason connect` minted it locally until `mason user add`
|
|
3
|
+
* replaced it, and the tests and an operator holding the API key still can — an HMAC-SHA256 over its expiry,
|
|
4
|
+
* keyed by the project's API key (`.mason/config.json` `apiKey`) — and the Postgres listener's `authenticate` verifies
|
|
5
|
+
* it. Unrestricted only: no scope, no service tokens, no revocation but rotating the API key. `mason_<expiry>_<mac>`:
|
|
6
|
+
* the expiry in seconds since the epoch, the MAC base64url.
|
|
7
|
+
*/
|
|
8
|
+
import * as crypto from "node:crypto";
|
|
9
|
+
const PREFIX = "mason";
|
|
10
|
+
/** a day, as the interactive tokens of the ClickHouse stack's Query API were */
|
|
11
|
+
export const MAX_TOKEN_SECONDS = 86_400;
|
|
12
|
+
const mac = (apiKey, expiry) => crypto.createHmac("sha256", apiKey).update(`mason-duckdb-postgres:${expiry}`).digest("base64url");
|
|
13
|
+
export function mintToken(apiKey, seconds, now = Date.now()) {
|
|
14
|
+
const expiry = Math.floor(now / 1000) + seconds;
|
|
15
|
+
return { token: `${PREFIX}_${expiry}_${mac(apiKey, expiry)}`, expiresAt: new Date(expiry * 1000).toISOString() };
|
|
16
|
+
}
|
|
17
|
+
/** null when the token is good; else why not, in a sentence a person reads after "password authentication failed" */
|
|
18
|
+
export function tokenProblem(apiKey, token, now = Date.now()) {
|
|
19
|
+
if (token === apiKey)
|
|
20
|
+
return "the API key is not a password here: mason user add <name> makes a user whose password does not expire";
|
|
21
|
+
const m = /^mason_(\d{1,12})_([A-Za-z0-9_-]{43})$/.exec(token);
|
|
22
|
+
if (m === null)
|
|
23
|
+
return "not a Mason user's password or token: mason user add <name> makes a user whose password does not expire";
|
|
24
|
+
const expiry = Number(m[1]);
|
|
25
|
+
const want = Buffer.from(mac(apiKey, expiry)), got = Buffer.from(m[2]);
|
|
26
|
+
if (want.length !== got.length || !crypto.timingSafeEqual(want, got))
|
|
27
|
+
return "not a password or token of this project (or its API key was rotated): mason user add <name> makes a user whose password does not expire";
|
|
28
|
+
if (expiry * 1000 <= now)
|
|
29
|
+
return `the token expired at ${new Date(expiry * 1000).toISOString()}: mason user add <name> makes a user whose password does not expire`;
|
|
30
|
+
return null;
|
|
31
|
+
}
|