@nshiab/simple-data-analysis-core 2.1.1 → 2.1.3
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/esm/class/SimpleDB.d.ts.map +1 -1
- package/esm/class/SimpleDB.js +23 -14
- package/esm/class/SimpleTable.d.ts +21 -6
- package/esm/class/SimpleTable.d.ts.map +1 -1
- package/esm/class/SimpleTable.js +23 -8
- package/esm/helpers/cleanCache.d.ts +1 -1
- package/esm/helpers/cleanCache.d.ts.map +1 -1
- package/esm/helpers/cleanCache.js +2 -2
- package/esm/helpers/normalizeCacheCode.d.ts +8 -0
- package/esm/helpers/normalizeCacheCode.d.ts.map +1 -0
- package/esm/helpers/normalizeCacheCode.js +331 -0
- package/esm/helpers/serializeCacheInputs.d.ts.map +1 -1
- package/esm/helpers/serializeCacheInputs.js +2 -1
- package/esm/helpers/updateColumnsWithJS.d.ts.map +1 -1
- package/esm/helpers/updateColumnsWithJS.js +14 -12
- package/esm/methods/cache.d.ts.map +1 -1
- package/esm/methods/cache.js +2 -1
- package/esm/methods/similarityMahalanobis.d.ts +5 -0
- package/esm/methods/similarityMahalanobis.d.ts.map +1 -0
- package/esm/methods/{mahalanobis.js → similarityMahalanobis.js} +9 -9
- package/esm/methods/updateWithJS.js +24 -5
- package/llm.md +22 -10
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/script/class/SimpleDB.d.ts.map +1 -1
- package/script/class/SimpleDB.js +23 -14
- package/script/class/SimpleTable.d.ts +21 -6
- package/script/class/SimpleTable.d.ts.map +1 -1
- package/script/class/SimpleTable.js +23 -8
- package/script/helpers/cleanCache.d.ts +1 -1
- package/script/helpers/cleanCache.d.ts.map +1 -1
- package/script/helpers/cleanCache.js +2 -2
- package/script/helpers/normalizeCacheCode.d.ts +8 -0
- package/script/helpers/normalizeCacheCode.d.ts.map +1 -0
- package/script/helpers/normalizeCacheCode.js +334 -0
- package/script/helpers/serializeCacheInputs.d.ts.map +1 -1
- package/script/helpers/serializeCacheInputs.js +2 -1
- package/script/helpers/updateColumnsWithJS.d.ts.map +1 -1
- package/script/helpers/updateColumnsWithJS.js +13 -11
- package/script/methods/cache.d.ts.map +1 -1
- package/script/methods/cache.js +2 -1
- package/script/methods/similarityMahalanobis.d.ts +5 -0
- package/script/methods/similarityMahalanobis.d.ts.map +1 -0
- package/script/methods/{mahalanobis.js → similarityMahalanobis.js} +10 -10
- package/script/methods/updateWithJS.js +24 -5
- package/esm/methods/mahalanobis.d.ts +0 -5
- package/esm/methods/mahalanobis.d.ts.map +0 -1
- package/script/methods/mahalanobis.d.ts +0 -5
- package/script/methods/mahalanobis.d.ts.map +0 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const lineTerminator = /[\r\n\u2028\u2029]/u;
|
|
2
|
+
const word = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*/u;
|
|
3
|
+
const number = /^(?:0[xX][\da-fA-F_]+n?|0[bB][01_]+n?|0[oO][0-7_]+n?|(?:\d[\d_]*(?:\.[\d_]*)?|\.[\d_]+)(?:[eE][+-]?[\d_]+)?n?)/;
|
|
4
|
+
const punctuation = /^(?:>>>=|===|!==|\*\*=|&&=|\|\|=|\?\?=|>>>|<<=|>>=|=>|==|!=|<=|>=|\+\+|--|&&|\|\||\?\?|\?\.(?!\d)|\*\*|<<|>>|\+=|-=|\*=|%=|&=|\|=|\^=|\.\.\.|[{}()[\].,;:?~!+*%<>=&|^\-#])/;
|
|
5
|
+
const expressionPrefixes = new Set([
|
|
6
|
+
"(",
|
|
7
|
+
"[",
|
|
8
|
+
"{",
|
|
9
|
+
",",
|
|
10
|
+
";",
|
|
11
|
+
":",
|
|
12
|
+
"?",
|
|
13
|
+
"=",
|
|
14
|
+
"=>",
|
|
15
|
+
"!",
|
|
16
|
+
"~",
|
|
17
|
+
"+",
|
|
18
|
+
"-",
|
|
19
|
+
"*",
|
|
20
|
+
"**",
|
|
21
|
+
"%",
|
|
22
|
+
"&",
|
|
23
|
+
"|",
|
|
24
|
+
"^",
|
|
25
|
+
"&&",
|
|
26
|
+
"||",
|
|
27
|
+
"??",
|
|
28
|
+
"==",
|
|
29
|
+
"===",
|
|
30
|
+
"!=",
|
|
31
|
+
"!==",
|
|
32
|
+
"<",
|
|
33
|
+
">",
|
|
34
|
+
"<=",
|
|
35
|
+
">=",
|
|
36
|
+
"<<",
|
|
37
|
+
">>",
|
|
38
|
+
">>>",
|
|
39
|
+
"+=",
|
|
40
|
+
"-=",
|
|
41
|
+
"*=",
|
|
42
|
+
"**=",
|
|
43
|
+
"%=",
|
|
44
|
+
"&=",
|
|
45
|
+
"|=",
|
|
46
|
+
"^=",
|
|
47
|
+
"&&=",
|
|
48
|
+
"||=",
|
|
49
|
+
"??=",
|
|
50
|
+
"<<=",
|
|
51
|
+
">>=",
|
|
52
|
+
">>>=",
|
|
53
|
+
"/",
|
|
54
|
+
"/=",
|
|
55
|
+
]);
|
|
56
|
+
const expressionKeywords = new Set([
|
|
57
|
+
"return",
|
|
58
|
+
"throw",
|
|
59
|
+
"case",
|
|
60
|
+
"delete",
|
|
61
|
+
"void",
|
|
62
|
+
"typeof",
|
|
63
|
+
"new",
|
|
64
|
+
"in",
|
|
65
|
+
"instanceof",
|
|
66
|
+
"yield",
|
|
67
|
+
"await",
|
|
68
|
+
"else",
|
|
69
|
+
"do",
|
|
70
|
+
]);
|
|
71
|
+
const restrictedLineBreaks = new Set([
|
|
72
|
+
"return",
|
|
73
|
+
"throw",
|
|
74
|
+
"break",
|
|
75
|
+
"continue",
|
|
76
|
+
"yield",
|
|
77
|
+
"async",
|
|
78
|
+
"++",
|
|
79
|
+
"--",
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Normalizes comments, whitespace, and known optional punctuation without
|
|
83
|
+
* dependencies or executing user code. Literal contents and token boundaries
|
|
84
|
+
* stay intact. Potentially significant line breaks are retained; ambiguous
|
|
85
|
+
* slash syntax falls back to exact source instead of risking a stale cache hit.
|
|
86
|
+
*/
|
|
87
|
+
export default function normalizeCacheCode(source) {
|
|
88
|
+
let position = 0;
|
|
89
|
+
let uncertain = false;
|
|
90
|
+
function quoted(quote) {
|
|
91
|
+
const start = position++;
|
|
92
|
+
while (position < source.length) {
|
|
93
|
+
const character = source[position++];
|
|
94
|
+
if (character === "\\") {
|
|
95
|
+
position++;
|
|
96
|
+
}
|
|
97
|
+
else if (character === quote) {
|
|
98
|
+
return source.slice(start, position);
|
|
99
|
+
}
|
|
100
|
+
else if (lineTerminator.test(character)) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function template() {
|
|
106
|
+
const parts = [];
|
|
107
|
+
let start = ++position;
|
|
108
|
+
while (position < source.length) {
|
|
109
|
+
if (source[position] === "\\") {
|
|
110
|
+
position += 2;
|
|
111
|
+
}
|
|
112
|
+
else if (source[position] === "`") {
|
|
113
|
+
parts.push(source.slice(start, position++));
|
|
114
|
+
return JSON.stringify(parts);
|
|
115
|
+
}
|
|
116
|
+
else if (source.startsWith("${", position)) {
|
|
117
|
+
parts.push(source.slice(start, position));
|
|
118
|
+
position += 2;
|
|
119
|
+
const tokens = scan(true);
|
|
120
|
+
if (uncertain)
|
|
121
|
+
return undefined;
|
|
122
|
+
parts.push(serialize(tokens));
|
|
123
|
+
start = position;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
position++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function regex() {
|
|
131
|
+
const start = position++;
|
|
132
|
+
let inClass = false;
|
|
133
|
+
while (position < source.length) {
|
|
134
|
+
const character = source[position++];
|
|
135
|
+
if (lineTerminator.test(character))
|
|
136
|
+
return undefined;
|
|
137
|
+
if (character === "\\") {
|
|
138
|
+
position++;
|
|
139
|
+
}
|
|
140
|
+
else if (character === "[") {
|
|
141
|
+
// Nested character classes depend on the v flag. Leave these opaque.
|
|
142
|
+
if (inClass)
|
|
143
|
+
return undefined;
|
|
144
|
+
inClass = true;
|
|
145
|
+
}
|
|
146
|
+
else if (character === "]") {
|
|
147
|
+
inClass = false;
|
|
148
|
+
}
|
|
149
|
+
else if (character === "/" && !inClass) {
|
|
150
|
+
const flags = source.slice(position).match(word)?.[0] ?? "";
|
|
151
|
+
position += flags.length;
|
|
152
|
+
return source.slice(start, position);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function scan(interpolation = false) {
|
|
157
|
+
const tokens = [];
|
|
158
|
+
let braces = 0;
|
|
159
|
+
let lineBreak = false;
|
|
160
|
+
while (position < source.length) {
|
|
161
|
+
const rest = source.slice(position);
|
|
162
|
+
const character = source[position];
|
|
163
|
+
if (/\s/u.test(character)) {
|
|
164
|
+
lineBreak ||= lineTerminator.test(character);
|
|
165
|
+
position++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (rest.startsWith("//")) {
|
|
169
|
+
while (position < source.length && !lineTerminator.test(source[position])) {
|
|
170
|
+
position++;
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (rest.startsWith("/*")) {
|
|
175
|
+
const end = source.indexOf("*/", position + 2);
|
|
176
|
+
if (end === -1)
|
|
177
|
+
break;
|
|
178
|
+
lineBreak ||= lineTerminator.test(source.slice(position, end + 2));
|
|
179
|
+
position = end + 2;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (interpolation && character === "}" && braces === 0) {
|
|
183
|
+
position++;
|
|
184
|
+
return tokens;
|
|
185
|
+
}
|
|
186
|
+
// Legacy HTML comments and native function text are not ordinary tokens.
|
|
187
|
+
if (rest.startsWith("<!--") || rest.startsWith("-->") ||
|
|
188
|
+
rest.startsWith("[native code]"))
|
|
189
|
+
break;
|
|
190
|
+
const previous = tokens.at(-1);
|
|
191
|
+
let kind = "punctuation";
|
|
192
|
+
let text;
|
|
193
|
+
if (character === "'" || character === '"') {
|
|
194
|
+
kind = "literal";
|
|
195
|
+
text = quoted(character);
|
|
196
|
+
}
|
|
197
|
+
else if (character === "`") {
|
|
198
|
+
kind = "template";
|
|
199
|
+
text = template();
|
|
200
|
+
}
|
|
201
|
+
else if (character === "/") {
|
|
202
|
+
const property = [".", "?."].includes(tokens.at(-2)?.text ?? "");
|
|
203
|
+
if (!previous ||
|
|
204
|
+
(previous.kind === "punctuation" &&
|
|
205
|
+
expressionPrefixes.has(previous.text)) ||
|
|
206
|
+
(previous.kind === "word" && !property &&
|
|
207
|
+
expressionKeywords.has(previous.text))) {
|
|
208
|
+
kind = "literal";
|
|
209
|
+
text = regex();
|
|
210
|
+
}
|
|
211
|
+
else if (previous.kind === "literal" || previous.kind === "template") {
|
|
212
|
+
text = rest.startsWith("/=") ? "/=" : "/";
|
|
213
|
+
position += text.length;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// After a closing block/parenthesis or contextual keyword, deciding
|
|
217
|
+
// between division and a regex can require a full JavaScript parser.
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
text = rest.match(word)?.[0];
|
|
223
|
+
if (text !== undefined) {
|
|
224
|
+
kind = "word";
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
text = rest.match(number)?.[0];
|
|
228
|
+
if (text !== undefined)
|
|
229
|
+
kind = "literal";
|
|
230
|
+
else
|
|
231
|
+
text = rest.match(punctuation)?.[0];
|
|
232
|
+
}
|
|
233
|
+
if (text !== undefined)
|
|
234
|
+
position += text.length;
|
|
235
|
+
}
|
|
236
|
+
if (text === undefined) {
|
|
237
|
+
uncertain = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (kind === "punctuation") {
|
|
241
|
+
if (text === "{")
|
|
242
|
+
braces++;
|
|
243
|
+
if (text === "}")
|
|
244
|
+
braces--;
|
|
245
|
+
}
|
|
246
|
+
tokens.push({ kind, text, lineBreak });
|
|
247
|
+
lineBreak = false;
|
|
248
|
+
}
|
|
249
|
+
if (position < source.length || interpolation)
|
|
250
|
+
uncertain = true;
|
|
251
|
+
return tokens;
|
|
252
|
+
}
|
|
253
|
+
const tokens = scan();
|
|
254
|
+
return uncertain
|
|
255
|
+
? JSON.stringify(["source", source])
|
|
256
|
+
: JSON.stringify(["tokens", serialize(tokens)]);
|
|
257
|
+
}
|
|
258
|
+
function serialize(tokens) {
|
|
259
|
+
tokens = normalizePunctuation(tokens);
|
|
260
|
+
return JSON.stringify(tokens.map((token, index) => {
|
|
261
|
+
const previous = tokens[index - 1];
|
|
262
|
+
// Only discard newlines where continuation is certain. Keeping other
|
|
263
|
+
// newlines permits conservative misses rather than unsafe cache hits.
|
|
264
|
+
const significant = token.lineBreak && previous !== undefined && (restrictedLineBreaks.has(previous.text) ||
|
|
265
|
+
["++", "--"].includes(token.text) ||
|
|
266
|
+
!((previous.kind === "punctuation" &&
|
|
267
|
+
(expressionPrefixes.has(previous.text) ||
|
|
268
|
+
[".", "?.", "}"].includes(previous.text))) ||
|
|
269
|
+
(token.kind === "punctuation" &&
|
|
270
|
+
([")", "]", "}", ",", ";", ".", "?."].includes(token.text) ||
|
|
271
|
+
(expressionPrefixes.has(token.text) &&
|
|
272
|
+
!["(", "[", "{", "!", "~"].includes(token.text))))));
|
|
273
|
+
return [token.kind, token.text, significant];
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
function normalizePunctuation(input) {
|
|
277
|
+
const tokens = input.map((token) => ({ ...token }));
|
|
278
|
+
const result = [];
|
|
279
|
+
const brackets = [];
|
|
280
|
+
const controls = new Set(["if", "while", "for", "with", "switch", "catch"]);
|
|
281
|
+
let previousControl = false;
|
|
282
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
283
|
+
const token = tokens[i];
|
|
284
|
+
const next = tokens[i + 1];
|
|
285
|
+
const previous = result.at(-1);
|
|
286
|
+
if (token.kind === "literal" && /^["']/.test(token.text) &&
|
|
287
|
+
!token.text.includes("\\")) {
|
|
288
|
+
token.text = JSON.stringify(token.text.slice(1, -1));
|
|
289
|
+
}
|
|
290
|
+
// A single arrow parameter has the same binding with or without parentheses.
|
|
291
|
+
if (token.text === "(" && tokens[i + 1]?.kind === "word" &&
|
|
292
|
+
tokens[i + 2]?.text === ")" && tokens[i + 3]?.text === "=>") {
|
|
293
|
+
result.push({ ...tokens[i + 1], lineBreak: token.lineBreak });
|
|
294
|
+
i += 2;
|
|
295
|
+
previousControl = false;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// Retain commas representing array holes, including the final hole.
|
|
299
|
+
if (token.text === "," && [")", "]", "}"].includes(next?.text ?? "") &&
|
|
300
|
+
previous && ![",", "[", "("].includes(previous.text))
|
|
301
|
+
continue;
|
|
302
|
+
const expressionEnd = previous && (previous.kind !== "punctuation" || previous.text === "]" ||
|
|
303
|
+
(previous.text === ")" && !previousControl));
|
|
304
|
+
if (token.text === ";" && brackets.at(-1)?.text === "{" && expressionEnd &&
|
|
305
|
+
!restrictedLineBreaks.has(previous.text)) {
|
|
306
|
+
if (next?.text === "}")
|
|
307
|
+
continue;
|
|
308
|
+
// A call followed by an identifier starts a new statement. Preserve that
|
|
309
|
+
// boundary as a newline so explicit and automatically inserted semicolons
|
|
310
|
+
// agree. Never do this in a for header or after a control header.
|
|
311
|
+
if (previous.text === ")" && next?.kind === "word" &&
|
|
312
|
+
!["in", "instanceof", "of"].includes(next.text)) {
|
|
313
|
+
next.lineBreak = true;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
previousControl = false;
|
|
318
|
+
if (["(", "[", "{"].includes(token.text)) {
|
|
319
|
+
brackets.push({
|
|
320
|
+
text: token.text,
|
|
321
|
+
control: token.text === "(" && (controls.has(previous?.text ?? "") ||
|
|
322
|
+
(previous?.text === "await" && result.at(-2)?.text === "for")),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else if ([")", "]", "}"].includes(token.text)) {
|
|
326
|
+
previousControl = brackets.pop()?.control ?? false;
|
|
327
|
+
}
|
|
328
|
+
result.push(token);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializeCacheInputs.d.ts","sourceRoot":"","sources":["../../src/helpers/serializeCacheInputs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"serializeCacheInputs.d.ts","sourceRoot":"","sources":["../../src/helpers/serializeCacheInputs.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAC1C,MAAM,EAAE,SAAS,OAAO,EAAE,GACzB,MAAM,CASR;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIrE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import SimpleTable from "../class/SimpleTable.js";
|
|
2
2
|
import { getTableGeneration } from "./tableGeneration.js";
|
|
3
|
+
import normalizeCacheCode from "./normalizeCacheCode.js";
|
|
3
4
|
/**
|
|
4
5
|
* Converts cache inputs to a deterministic, type-tagged representation.
|
|
5
6
|
* SimpleTable values are represented by their name and current generation.
|
|
@@ -34,7 +35,7 @@ function serializeValue(value, path, ancestors) {
|
|
|
34
35
|
case "function":
|
|
35
36
|
return JSON.stringify([
|
|
36
37
|
"function",
|
|
37
|
-
Function.prototype.toString.call(value),
|
|
38
|
+
normalizeCacheCode(Function.prototype.toString.call(value)),
|
|
38
39
|
]);
|
|
39
40
|
case "symbol":
|
|
40
41
|
throw new TypeError(`cache() ${path} cannot be a symbol.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updateColumnsWithJS.d.ts","sourceRoot":"","sources":["../../src/helpers/updateColumnsWithJS.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"updateColumnsWithJS.d.ts","sourceRoot":"","sources":["../../src/helpers/updateColumnsWithJS.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAQvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAA8B,mBAAmB,CAC/C,KAAK,EAAE,WAAW,EAClB,YAAY,EAAE,MAAM,EAAE,EACtB,aAAa,EAAE,MAAM,EAAE,EACvB,QAAQ,EAAE,CACR,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,KAC/B,OAAO,CAAC;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,CAAC,EAC1C,OAAO,GAAE;IACP;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACf,GACL,OAAO,CAAC,IAAI,CAAC,CA0Jf"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import quoteIdentifier from "./quoteIdentifier.js";
|
|
2
2
|
import readMutationRows from "./readMutationRows.js";
|
|
3
3
|
import { retainRegisteredTables } from "./tableRegistry.js";
|
|
4
|
-
import {
|
|
4
|
+
import { prepareArray } from "../methods/loadArray.js";
|
|
5
|
+
import appendColumnBatches from "./appendColumnBatches.js";
|
|
6
|
+
import parseDuckDBType from "./parseDuckDBType.js";
|
|
5
7
|
/**
|
|
6
8
|
* Generates columns from selected input columns, keeping other SQL values in
|
|
7
9
|
* DuckDB. Call from an asynchronous extension barrier. The callback must return
|
|
@@ -68,7 +70,6 @@ export default async function updateColumnsWithJS(table, inputColumns, outputCol
|
|
|
68
70
|
const id = `__sda_id_${suffix}`;
|
|
69
71
|
const snapshot = `__sda_source_${suffix}`;
|
|
70
72
|
const staged = table.sdb.newTable(`__sda_generated_${suffix}`);
|
|
71
|
-
const batch = table.sdb.newTable(`__sda_batch_${suffix}`);
|
|
72
73
|
const q = quoteIdentifier;
|
|
73
74
|
try {
|
|
74
75
|
// A SQL snapshot gives every row a stable identity, including duplicate rows
|
|
@@ -92,17 +93,17 @@ export default async function updateColumnsWithJS(table, inputColumns, outputCol
|
|
|
92
93
|
throw new Error("Column generation must return one row per input row.");
|
|
93
94
|
}
|
|
94
95
|
// Generated vectors retain this helper's existing inference contract.
|
|
95
|
-
|
|
96
|
+
const prepared = prepareArray(generated.map((row, i) => ({
|
|
96
97
|
...Object.fromEntries(outputColumns.map((name) => [name, row[name]])),
|
|
97
98
|
[id]: ids[i],
|
|
98
|
-
})))
|
|
99
|
-
const batchTypes =
|
|
99
|
+
})));
|
|
100
|
+
const batchTypes = new Map(prepared.keys.map((name, i) => [name, prepared.types[i]]));
|
|
100
101
|
for (const name of outputColumns) {
|
|
101
102
|
const nonNull = generated.some((row) => row[name] !== null && row[name] !== undefined);
|
|
102
103
|
if (!nonNull)
|
|
103
104
|
continue;
|
|
104
105
|
const previous = knownTypes.get(name);
|
|
105
|
-
const current = batchTypes
|
|
106
|
+
const current = batchTypes.get(name);
|
|
106
107
|
if (previous !== undefined && previous !== current) {
|
|
107
108
|
throw new Error(`Generated column ${JSON.stringify(name)} changed type from ${previous} to ${current}.`);
|
|
108
109
|
}
|
|
@@ -114,12 +115,13 @@ export default async function updateColumnsWithJS(table, inputColumns, outputCol
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
if (!hasRows) {
|
|
117
|
-
await table.sdb.customQuery(`CREATE TEMP TABLE ${q(staged.name)}
|
|
118
|
+
await table.sdb.customQuery(`CREATE TEMP TABLE ${q(staged.name)} (${prepared.keys.map((name, i) => `${q(name)} ${prepared.types[i]}`)
|
|
119
|
+
.join(", ")})`);
|
|
118
120
|
hasRows = true;
|
|
119
121
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
// All-null batches use the column's established type, avoiding a separate
|
|
123
|
+
// batch table and SQL copy just to cast nulls into the staged schema.
|
|
124
|
+
await appendColumnBatches(table.connection, staged.name, prepared.keys.map((name, i) => parseDuckDBType(knownTypes.get(name) ?? prepared.types[i])), prepared.rowCount, (column, start, end) => prepared.columnsData[column].slice(start, end));
|
|
123
125
|
}
|
|
124
126
|
if (!hasRows)
|
|
125
127
|
return;
|
|
@@ -139,7 +141,7 @@ export default async function updateColumnsWithJS(table, inputColumns, outputCol
|
|
|
139
141
|
await table.sdb.customQuery(`CREATE OR REPLACE TABLE ${q(table.name)} AS SELECT ${columns.join(", ")} FROM ${q(snapshot)} original JOIN ${q(staged.name)} generated ON original.${q(id)} = CAST(generated.${q(id)} AS BIGINT) ORDER BY original.${q(id)}`);
|
|
140
142
|
}
|
|
141
143
|
finally {
|
|
142
|
-
await table.sdb.customQuery(`DROP TABLE IF EXISTS ${q(snapshot)}; DROP TABLE IF EXISTS ${q(staged.name)}
|
|
143
|
-
retainRegisteredTables(table.sdb, (item) => item !== staged
|
|
144
|
+
await table.sdb.customQuery(`DROP TABLE IF EXISTS ${q(snapshot)}; DROP TABLE IF EXISTS ${q(staged.name)};`);
|
|
145
|
+
retainRegisteredTables(table.sdb, (item) => item !== staged);
|
|
144
146
|
}
|
|
145
147
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/methods/cache.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/methods/cache.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AA6DvD,wBAA8B,KAAK,CAAC,KAAK,SAAS,WAAW,EAC3D,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC/C,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,iBA4LP"}
|
package/esm/methods/cache.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "nod
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import flushAllTables from "../helpers/flushAllTables.js";
|
|
4
4
|
import prettyDuration from "../helpers/prettyDuration.js";
|
|
5
|
+
import normalizeCacheCode from "../helpers/normalizeCacheCode.js";
|
|
5
6
|
import { cacheEntryExpired, cacheLoadMessage, cacheTtlMessage, } from "../helpers/cacheDiagnostics.js";
|
|
6
7
|
import serializeCacheInputs, { getCacheInputLabel, } from "../helpers/serializeCacheInputs.js";
|
|
7
8
|
import { createTableGenerationId, peekTableGeneration, restoreTableGeneration, } from "../helpers/tableGeneration.js";
|
|
@@ -27,7 +28,7 @@ export default async function cache(table, compute, options = {}) {
|
|
|
27
28
|
if (existsSync(cacheSourcesPath)) {
|
|
28
29
|
cacheSources = JSON.parse(readFileSync(cacheSourcesPath, "utf-8"));
|
|
29
30
|
}
|
|
30
|
-
const functionBody = compute.toString();
|
|
31
|
+
const functionBody = normalizeCacheCode(compute.toString());
|
|
31
32
|
const entryGeneration = peekTableGeneration(table) ?? null;
|
|
32
33
|
const inputs = options.inputs?.filter((input) => input !== table) ?? [];
|
|
33
34
|
const hasInputs = inputs.length > 0;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type SimpleTable from "../class/SimpleTable.js";
|
|
2
|
+
type Options = NonNullable<Parameters<SimpleTable["similarityMahalanobis"]>[3]>;
|
|
3
|
+
export default function similarityMahalanobis(table: SimpleTable, columns: string | string[], referencePoint: number[], newColumn: string, options?: Options): void;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=similarityMahalanobis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similarityMahalanobis.d.ts","sourceRoot":"","sources":["../../src/methods/similarityMahalanobis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AASvD,KAAK,OAAO,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEhF,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAC3C,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAC1B,cAAc,EAAE,MAAM,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,OAAY,GACpB,IAAI,CAyBN"}
|
|
@@ -5,17 +5,17 @@ import prepareNumericFeatures from "../helpers/prepareNumericFeatures.js";
|
|
|
5
5
|
import publishPreparedColumns from "../helpers/publishPreparedColumns.js";
|
|
6
6
|
import queueOp from "../helpers/queueOp.js";
|
|
7
7
|
import quoteIdentifier from "../helpers/quoteIdentifier.js";
|
|
8
|
-
export default function
|
|
8
|
+
export default function similarityMahalanobis(table, columns, referencePoint, newColumn, options = {}) {
|
|
9
9
|
if (!Array.isArray(referencePoint) || referencePoint.length === 0 ||
|
|
10
10
|
Array.from(referencePoint).some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
11
|
-
throw new Error("
|
|
11
|
+
throw new Error("similarityMahalanobis() referencePoint must be a nonempty array of finite numbers in feature-dimension order.");
|
|
12
12
|
}
|
|
13
13
|
const selected = typeof columns === "string" ? columns : [...columns];
|
|
14
14
|
const reference = [...referencePoint];
|
|
15
15
|
const settings = { ...options };
|
|
16
16
|
queueOp(table, {
|
|
17
17
|
kind: "barrier",
|
|
18
|
-
method: "
|
|
18
|
+
method: "similarityMahalanobis()",
|
|
19
19
|
parameters: {
|
|
20
20
|
columns: selected,
|
|
21
21
|
referencePoint: reference,
|
|
@@ -33,23 +33,23 @@ async function execute(table, columns, referencePoint, newColumn, options) {
|
|
|
33
33
|
const seen = new Set();
|
|
34
34
|
for (const name of outputNames) {
|
|
35
35
|
if (typeof name !== "string" || name.length === 0 || name.includes("\0")) {
|
|
36
|
-
throw new Error("
|
|
36
|
+
throw new Error("similarityMahalanobis() output column names must be nonempty strings without null characters.");
|
|
37
37
|
}
|
|
38
38
|
const folded = foldIdentifier(name);
|
|
39
39
|
if (seen.has(folded)) {
|
|
40
|
-
throw new Error("
|
|
40
|
+
throw new Error("similarityMahalanobis() distance and similarity score columns must have different names (case-insensitive).");
|
|
41
41
|
}
|
|
42
42
|
seen.add(folded);
|
|
43
43
|
if (sourceColumns.some((column) => foldIdentifier(column) === folded)) {
|
|
44
|
-
throw new Error(`
|
|
44
|
+
throw new Error(`similarityMahalanobis() cannot create ${quoteIdentifier(name)} because that column already exists. Remove it first or choose a different name.`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
const prepared = await prepareNumericFeatures(table, typeof columns === "string"
|
|
48
48
|
? { kind: "vector", column: columns }
|
|
49
|
-
: { kind: "scalars", columns }, { method: "
|
|
49
|
+
: { kind: "scalars", columns }, { method: "similarityMahalanobis()" });
|
|
50
50
|
try {
|
|
51
51
|
if (referencePoint.length !== prepared.dimensions) {
|
|
52
|
-
throw new Error(`
|
|
52
|
+
throw new Error(`similarityMahalanobis() referencePoint must contain ${prepared.dimensions} values in feature-dimension order; received ${referencePoint.length}.`);
|
|
53
53
|
}
|
|
54
54
|
const model = await computeCovariance(table.connection, {
|
|
55
55
|
relation: prepared.relation,
|
|
@@ -73,7 +73,7 @@ async function execute(table, columns, referencePoint, newColumn, options) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
await publishPreparedColumns(table, prepared, {
|
|
76
|
-
method: "
|
|
76
|
+
method: "similarityMahalanobis()",
|
|
77
77
|
parameters: { columns, referencePoint, newColumn, options },
|
|
78
78
|
result: {
|
|
79
79
|
relation: distances.relation,
|
|
@@ -60,6 +60,7 @@ async function executeUpdateWithJS(simpleTable, dataModifier, options) {
|
|
|
60
60
|
const accumulator = `updateWithJS_accumulator_${suffix}`;
|
|
61
61
|
try {
|
|
62
62
|
let first = true;
|
|
63
|
+
let accumulatorSchema;
|
|
63
64
|
let lastRowid = null;
|
|
64
65
|
let sawRows = false;
|
|
65
66
|
let outputOffset = 0;
|
|
@@ -131,9 +132,17 @@ async function executeUpdateWithJS(simpleTable, dataModifier, options) {
|
|
|
131
132
|
? parseDuckDBType("VARCHAR")
|
|
132
133
|
: source.types.get(key) ??
|
|
133
134
|
parseDuckDBType(added.types[newKeys.indexOf(key)]));
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
const schema = JSON.stringify(keys.map((key, i) => [key, columnTypes[i].toString()]));
|
|
136
|
+
// Append matching native batches directly. SQL still handles conversion
|
|
137
|
+
// and BY NAME alignment when a callback changes the batch schema.
|
|
138
|
+
const direct = outputGeometry.length === 0 && outputJSON.length === 0 &&
|
|
139
|
+
(first || schema === accumulatorSchema);
|
|
140
|
+
const destination = direct ? accumulator : scratch.name;
|
|
141
|
+
if (!direct || first) {
|
|
142
|
+
await simpleTable.sdb.customQuery(`CREATE OR REPLACE TABLE ${quoteIdentifier(destination)} (${keys.map((key, i) => `${quoteIdentifier(key)} ${columnTypes[i]}`)
|
|
143
|
+
.join(", ")})`);
|
|
144
|
+
}
|
|
145
|
+
await appendColumnBatches(simpleTable.connection, destination, columnTypes, modified.length, (column, start, end) => {
|
|
137
146
|
const key = keys[column];
|
|
138
147
|
return outputGeometry.includes(key)
|
|
139
148
|
? geometryData.get(key).slice(start, end)
|
|
@@ -146,7 +155,11 @@ async function executeUpdateWithJS(simpleTable, dataModifier, options) {
|
|
|
146
155
|
: outputJSON.includes(key)
|
|
147
156
|
? `${quoteIdentifier(key)}::JSON AS ${quoteIdentifier(key)}`
|
|
148
157
|
: quoteIdentifier(key)).join(", ");
|
|
149
|
-
if (
|
|
158
|
+
if (direct) {
|
|
159
|
+
accumulatorSchema = schema;
|
|
160
|
+
first = false;
|
|
161
|
+
}
|
|
162
|
+
else if (first) {
|
|
150
163
|
await simpleTable.sdb.customQuery(`CREATE OR REPLACE TABLE ${quoteIdentifier(accumulator)} AS SELECT ${projection} FROM ${quoteIdentifier(scratch.name)}`);
|
|
151
164
|
first = false;
|
|
152
165
|
}
|
|
@@ -159,7 +172,13 @@ async function executeUpdateWithJS(simpleTable, dataModifier, options) {
|
|
|
159
172
|
if (first) {
|
|
160
173
|
throw new Error("The dataModifier returned no rows. updateWithJS can't infer the table schema from zero rows.");
|
|
161
174
|
}
|
|
162
|
-
|
|
175
|
+
// Publish the completed ordinary table without copying its rows again.
|
|
176
|
+
// Qualify the creation namespace: a temporary table or a later search-path
|
|
177
|
+
// schema can shadow the name without being the destination of replacement.
|
|
178
|
+
const namespace = await simpleTable.connection.runAndReadAll("SELECT current_database(), current_schema()");
|
|
179
|
+
const prefix = namespace.getRows()[0].map((name) => quoteIdentifier(String(name))).join(".");
|
|
180
|
+
await simpleTable.sdb.customQuery(`DROP TABLE IF EXISTS ${prefix}.${quoteIdentifier(simpleTable.name)};
|
|
181
|
+
ALTER TABLE ${prefix}.${quoteIdentifier(accumulator)} RENAME TO ${quoteIdentifier(simpleTable.name)};`);
|
|
163
182
|
}
|
|
164
183
|
finally {
|
|
165
184
|
await simpleTable.sdb.customQuery(`DROP TABLE IF EXISTS ${quoteIdentifier(accumulator)};
|