Repository navigation
Expand file tree
/
Copy pathresult-fields.ts
More file actions
100 lines (95 loc) · 4.84 KB
/
Copy pathresult-fields.ts
File metadata and controls
100 lines (95 loc) · 4.84 KB
1
2
3
4
5
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* The columns of a result whose rows need not share one shape.
*
* A document store answers documents, and two documents of one collection may carry different keys. Taking
* the columns from the first row drops every key only a later row carries: the grid shows no column for it,
* and the CSV, SQL INSERT and DDL exports, which write `fields`, leave it out, while the JSON export keeps it.
* The columns are therefore the union of the keys the rows carry, first seen first, so a result whose rows
* do share one shape answers exactly the first row's keys in their order.
*
* One pass over every row's own keys, with a Set for membership: linear in the keys returned, so a wide
* result of many documents costs what reading its keys costs.
*
* It names no engine. The module is pure and imports nothing, so client code imports it too: the grid and
* masking read the names `uniqueFieldNames` makes.
*/
export function unionFields(rows: readonly object[]): string[] {
const fields = new Set<string>();
for (const row of rows) {
for (const key of Object.keys(row)) fields.add(key);
}
return [...fields];
}
/** The name a column the driver declares with no name is shown and keyed under, as SQL Server's own tools word it. */
export const UNNAMED_FIELD = "(No column name)";
/**
* The names a result's columns are keyed under, one per declared column, in order: each non-empty and
* different from every other, which is what `QueryResult.fields` promises.
*
* A driver may declare a column with no name (SQL Server leaves every unaliased expression unnamed) or two
* columns with one name (a join that projects `id` from both tables). A row keyed by name then loses a value,
* or carries an empty key no grid column can take. So a column with no name is named `UNNAMED_FIELD`, and a
* repeat is numbered `name (2)`, `name (3)` (`numberedRepeatBase` reads that form back). A number never
* produces a name the result itself declares, before or after the repeat, so no column the statement named is
* shown under another column's value.
*
* Read the rows positionally and key them by these names; reading them keyed by the declared names is what
* loses the value in the first place. Names that differ only in letter case stay apart, as row keys do.
*
* It names no engine.
*/
export function uniqueFieldNames(declared: readonly string[]): string[] {
const declaredNames = new Set(declared);
const given = new Set<string>();
// The next number to try for each base. A numbered name skipped once stays taken (it is declared, or already
// given), so each base's numbers are tried once in all, and the whole call is linear in the columns.
const nextRepeat = new Map<string, number>();
return declared.map((name) => {
const base = name === "" ? UNNAMED_FIELD : name;
// A column keeps its own declared name the first time; a name it did not declare must be one nothing declares.
let unique = base;
if (given.has(base) || (base !== name && declaredNames.has(base))) {
let repeat = nextRepeat.get(base) ?? 2;
while (given.has(`${base} (${repeat})`) || declaredNames.has(`${base} (${repeat})`)) repeat += 1;
unique = `${base} (${repeat})`;
nextRepeat.set(base, repeat + 1);
}
given.add(unique);
return unique;
});
}
const NUMBERED_REPEAT = /^(.*) \(\d+\)$/;
/**
* The name a numbered name `name (N)` repeats, or null for a name not of that form: the one reader of the
* format `uniqueFieldNames` writes, so a change to the format changes its readers with it. Any digits count,
* and the base is read once (`a (2) (2)` gives `a (2)`). A statement may alias a column in this form itself;
* the caller decides what that means for it.
*/
export function numberedRepeatBase(name: string): string | null {
return NUMBERED_REPEAT.exec(name)?.[1] ?? null;
}
/**
* Rows keyed by their own keys (a document store's), with the result's columns named as `QueryResult.fields`
* promises. The keys of one object are already distinct, so the only name `uniqueFieldNames` changes is an
* empty key, which a grid column cannot take: every row carrying it is keyed under the new name instead.
*
* A result whose names all stay is answered as it is, without a copy; otherwise every row is rebuilt with
* `Object.fromEntries`, so a `__proto__` key stays a plain key.
*/
export function uniquelyKeyedRows(
declared: readonly string[],
rows: Record<string, unknown>[],
): { fields: string[]; rows: Record<string, unknown>[] } {
const fields = uniqueFieldNames(declared);
const renamed = new Map<string, string>();
declared.forEach((name, position) => {
if (fields[position] !== name) renamed.set(name, fields[position]);
});
if (renamed.size === 0) return { fields, rows };
return {
fields,
rows: rows.map((row) =>
Object.fromEntries(Object.entries(row).map(([key, value]) => [renamed.get(key) ?? key, value])),
),
};
}