@c9up/eon 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.
- package/README.md +69 -0
- package/dist/EonProvider.d.ts +51 -0
- package/dist/EonProvider.d.ts.map +1 -0
- package/dist/EonProvider.js +154 -0
- package/dist/EonProvider.js.map +1 -0
- package/dist/connection/EonConnection.d.ts +116 -0
- package/dist/connection/EonConnection.d.ts.map +1 -0
- package/dist/connection/EonConnection.js +15 -0
- package/dist/connection/EonConnection.js.map +1 -0
- package/dist/connection/config.d.ts +56 -0
- package/dist/connection/config.d.ts.map +1 -0
- package/dist/connection/config.js +14 -0
- package/dist/connection/config.js.map +1 -0
- package/dist/connection/websocket.d.ts +16 -0
- package/dist/connection/websocket.d.ts.map +1 -0
- package/dist/connection/websocket.js +383 -0
- package/dist/connection/websocket.js.map +1 -0
- package/dist/decorators/superTable.d.ts +73 -0
- package/dist/decorators/superTable.d.ts.map +1 -0
- package/dist/decorators/superTable.js +98 -0
- package/dist/decorators/superTable.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/schemaless.d.ts +22 -0
- package/dist/ingest/schemaless.d.ts.map +1 -0
- package/dist/ingest/schemaless.js +161 -0
- package/dist/ingest/schemaless.js.map +1 -0
- package/dist/ingest/sql.d.ts +23 -0
- package/dist/ingest/sql.d.ts.map +1 -0
- package/dist/ingest/sql.js +59 -0
- package/dist/ingest/sql.js.map +1 -0
- package/dist/ingest/stmt.d.ts +82 -0
- package/dist/ingest/stmt.d.ts.map +1 -0
- package/dist/ingest/stmt.js +246 -0
- package/dist/ingest/stmt.js.map +1 -0
- package/dist/metadata-keys.d.ts +17 -0
- package/dist/metadata-keys.d.ts.map +1 -0
- package/dist/metadata-keys.js +17 -0
- package/dist/metadata-keys.js.map +1 -0
- package/dist/query/TimeSeriesQuery.d.ts +121 -0
- package/dist/query/TimeSeriesQuery.d.ts.map +1 -0
- package/dist/query/TimeSeriesQuery.js +291 -0
- package/dist/query/TimeSeriesQuery.js.map +1 -0
- package/dist/query/native.d.ts +22 -0
- package/dist/query/native.d.ts.map +1 -0
- package/dist/query/native.js +135 -0
- package/dist/query/native.js.map +1 -0
- package/dist/repository/SuperTableRepository.d.ts +80 -0
- package/dist/repository/SuperTableRepository.d.ts.map +1 -0
- package/dist/repository/SuperTableRepository.js +232 -0
- package/dist/repository/SuperTableRepository.js.map +1 -0
- package/dist/schema/CreateStableSpec.d.ts +140 -0
- package/dist/schema/CreateStableSpec.d.ts.map +1 -0
- package/dist/schema/CreateStableSpec.js +41 -0
- package/dist/schema/CreateStableSpec.js.map +1 -0
- package/dist/schema/EonMigrationRunner.d.ts +71 -0
- package/dist/schema/EonMigrationRunner.d.ts.map +1 -0
- package/dist/schema/EonMigrationRunner.js +295 -0
- package/dist/schema/EonMigrationRunner.js.map +1 -0
- package/dist/schema/EonSchema.d.ts +128 -0
- package/dist/schema/EonSchema.d.ts.map +1 -0
- package/dist/schema/EonSchema.js +309 -0
- package/dist/schema/EonSchema.js.map +1 -0
- package/dist/schema/Migration.d.ts +47 -0
- package/dist/schema/Migration.d.ts.map +1 -0
- package/dist/schema/Migration.js +55 -0
- package/dist/schema/Migration.js.map +1 -0
- package/dist/schema/compile.d.ts +33 -0
- package/dist/schema/compile.d.ts.map +1 -0
- package/dist/schema/compile.js +70 -0
- package/dist/schema/compile.js.map +1 -0
- package/dist/schema/sync.d.ts +45 -0
- package/dist/schema/sync.d.ts.map +1 -0
- package/dist/schema/sync.js +102 -0
- package/dist/schema/sync.js.map +1 -0
- package/dist/services/connection.d.ts +33 -0
- package/dist/services/connection.d.ts.map +1 -0
- package/dist/services/connection.js +71 -0
- package/dist/services/connection.js.map +1 -0
- package/dist/testing/Factory.d.ts +43 -0
- package/dist/testing/Factory.d.ts.map +1 -0
- package/dist/testing/Factory.js +134 -0
- package/dist/testing/Factory.js.map +1 -0
- package/dist/testing/FakeEonConnection.d.ts +43 -0
- package/dist/testing/FakeEonConnection.d.ts.map +1 -0
- package/dist/testing/FakeEonConnection.js +315 -0
- package/dist/testing/FakeEonConnection.js.map +1 -0
- package/dist/testing/index.d.ts +34 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +67 -0
- package/dist/testing/index.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +65 -0
- package/scripts/copy-napi.mjs +72 -0
- package/src/EonProvider.ts +222 -0
- package/src/connection/EonConnection.ts +146 -0
- package/src/connection/config.ts +60 -0
- package/src/connection/websocket.ts +436 -0
- package/src/decorators/superTable.ts +169 -0
- package/src/index.ts +118 -0
- package/src/ingest/schemaless.ts +211 -0
- package/src/ingest/sql.ts +73 -0
- package/src/ingest/stmt.ts +335 -0
- package/src/metadata-keys.ts +20 -0
- package/src/query/TimeSeriesQuery.ts +480 -0
- package/src/query/native.ts +180 -0
- package/src/repository/SuperTableRepository.ts +347 -0
- package/src/schema/CreateStableSpec.ts +184 -0
- package/src/schema/EonMigrationRunner.ts +378 -0
- package/src/schema/EonSchema.ts +396 -0
- package/src/schema/Migration.ts +66 -0
- package/src/schema/compile.ts +109 -0
- package/src/schema/sync.ts +142 -0
- package/src/services/connection.ts +89 -0
- package/src/testing/Factory.ts +186 -0
- package/src/testing/FakeEonConnection.ts +351 -0
- package/src/testing/index.ts +81 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STMT columnar bulk-ingest planning (story 58.4) — the default high-throughput
|
|
3
|
+
* write path.
|
|
4
|
+
*
|
|
5
|
+
* This module turns plain point objects into the transport-agnostic
|
|
6
|
+
* {@link EonColumnarIngest} request: it groups points by their (deterministic)
|
|
7
|
+
* child table, accumulates **struct-of-arrays columns** per child (one typed
|
|
8
|
+
* array per column, NOT an array of row objects — the locked 58.0 contract, kept
|
|
9
|
+
* even on the ws path), chunks each child at `batchSize`, and carries the
|
|
10
|
+
* compiler-produced STMT template. The actual `stmtInit`/`bind`/`exec` happens
|
|
11
|
+
* in the transport (`EonConnection.ingestColumnar`); this module never touches a
|
|
12
|
+
* ws-specific type. The SQL template is produced ONLY by the Rust compiler — the
|
|
13
|
+
* injection seam stays in Rust (memory `feedback_security_first`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
EonBindKind,
|
|
18
|
+
EonBoundColumn,
|
|
19
|
+
EonChildBatch,
|
|
20
|
+
EonColumnarIngest,
|
|
21
|
+
} from "../connection/EonConnection.js";
|
|
22
|
+
import { compileStatementNative } from "../query/native.js";
|
|
23
|
+
import { childTableName, stringifyTagValue } from "../schema/sync.js";
|
|
24
|
+
|
|
25
|
+
/** The default STMT batch chunk (rows per child per bind). Overridable (OQ3). */
|
|
26
|
+
export const DEFAULT_BATCH_SIZE = 4096;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Logical column/tag type → the columnar bind kind that selects the transport
|
|
30
|
+
* setter. Typed precisely as `EonBindKind` (unlike the string-valued
|
|
31
|
+
* `TYPE_KIND_MAP`) so the plan carries a validated kind with no cast. Same
|
|
32
|
+
* logical alias set as the DDL type map.
|
|
33
|
+
*/
|
|
34
|
+
const BIND_KIND_BY_LOGICAL: Record<string, EonBindKind> = {
|
|
35
|
+
timestamp: "timestamp",
|
|
36
|
+
int: "int",
|
|
37
|
+
integer: "int",
|
|
38
|
+
bigint: "bigInt",
|
|
39
|
+
biginteger: "bigInt",
|
|
40
|
+
smallint: "smallInt",
|
|
41
|
+
tinyint: "tinyInt",
|
|
42
|
+
float: "float",
|
|
43
|
+
double: "double",
|
|
44
|
+
bool: "bool",
|
|
45
|
+
boolean: "bool",
|
|
46
|
+
string: "varchar",
|
|
47
|
+
varchar: "varchar",
|
|
48
|
+
nchar: "nchar",
|
|
49
|
+
binary: "varbinary",
|
|
50
|
+
varbinary: "varbinary",
|
|
51
|
+
json: "json",
|
|
52
|
+
decimal: "decimal",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Resolve a logical column/tag type to its bind kind, or throw a typed error. */
|
|
56
|
+
export function toBindKind(
|
|
57
|
+
logicalType: string | undefined,
|
|
58
|
+
property: string,
|
|
59
|
+
): EonBindKind {
|
|
60
|
+
const kind = BIND_KIND_BY_LOGICAL[(logicalType ?? "").toLowerCase()];
|
|
61
|
+
if (!kind) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[E_EON_TYPE] column/tag '${property}' has an unknown or missing type '${logicalType ?? ""}'`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return kind;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A column (or tag) in the ingest plan: its entity property + bind kind. */
|
|
70
|
+
export interface PlanColumn {
|
|
71
|
+
readonly property: string;
|
|
72
|
+
readonly kind: EonBindKind;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Resolved plan for one super-table's STMT ingest (built once per repository). */
|
|
76
|
+
export interface ColumnarPlan {
|
|
77
|
+
readonly stable: string;
|
|
78
|
+
readonly templateSql: string;
|
|
79
|
+
readonly tsProperty: string;
|
|
80
|
+
/** Value columns, timestamp first (prepared bind order). */
|
|
81
|
+
readonly columns: readonly PlanColumn[];
|
|
82
|
+
readonly tags: readonly PlanColumn[];
|
|
83
|
+
readonly batchSize: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A point: a plain object keyed by the entity's property names. */
|
|
87
|
+
export type IngestPoint = Record<string, unknown>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compile the STMT prepare template for a super-table via the Rust compiler
|
|
91
|
+
* (`INSERT INTO ? USING <stable> (<tagcols>) TAGS (?) VALUES (?)`). The single
|
|
92
|
+
* SQL authority stays in Rust — this never string-builds SQL.
|
|
93
|
+
*/
|
|
94
|
+
export function compileStmtTemplate(
|
|
95
|
+
stable: string,
|
|
96
|
+
tagColumns: readonly string[],
|
|
97
|
+
columns: readonly string[],
|
|
98
|
+
): string {
|
|
99
|
+
const compiled = compileStatementNative(
|
|
100
|
+
{
|
|
101
|
+
kind: "stmtInsertTemplate",
|
|
102
|
+
using: stable,
|
|
103
|
+
tagColumns: [...tagColumns],
|
|
104
|
+
columns: [...columns],
|
|
105
|
+
},
|
|
106
|
+
"tdengine",
|
|
107
|
+
);
|
|
108
|
+
const sql = compiled.statements[0];
|
|
109
|
+
if (sql === undefined) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"[E_EON_TEMPLATE] the compiler returned no statement for the STMT insert template",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return sql;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A child table's grouped rows + the tag values that route to it. */
|
|
118
|
+
export interface ChildGroup {
|
|
119
|
+
readonly table: string;
|
|
120
|
+
readonly tagValues: unknown[];
|
|
121
|
+
readonly rows: IngestPoint[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Group points by their deterministic child table (same tags → same child).
|
|
126
|
+
* Tag values are normalised (`undefined` → `null`) so routing is stable and a
|
|
127
|
+
* missing tag can never desync the FNV child name.
|
|
128
|
+
*/
|
|
129
|
+
export function groupByChild(
|
|
130
|
+
stable: string,
|
|
131
|
+
tagProperties: readonly string[],
|
|
132
|
+
points: readonly IngestPoint[],
|
|
133
|
+
): ChildGroup[] {
|
|
134
|
+
const groups = new Map<string, ChildGroup>();
|
|
135
|
+
for (const point of points) {
|
|
136
|
+
const tagValues = tagProperties.map((p) => point[p] ?? null);
|
|
137
|
+
const table = childTableName(stable, tagValues);
|
|
138
|
+
let group = groups.get(table);
|
|
139
|
+
if (!group) {
|
|
140
|
+
group = { table, tagValues, rows: [] };
|
|
141
|
+
groups.set(table, group);
|
|
142
|
+
} else if (!sameTagValues(group.tagValues, tagValues)) {
|
|
143
|
+
// Defence-in-depth behind the 64-bit child name: if two DISTINCT
|
|
144
|
+
// tag-sets ever hashed to the same child, later points would bind under
|
|
145
|
+
// the first set's tags (silent cross-series corruption). Refuse loudly
|
|
146
|
+
// instead — astronomically unlikely at 64 bits, never silent.
|
|
147
|
+
throw new Error(
|
|
148
|
+
`[E_EON_CHILD_COLLISION] two distinct tag-sets map to the same child table '${table}'; refusing to write mismatched rows under one tag-set.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
group.rows.push(point);
|
|
152
|
+
}
|
|
153
|
+
return [...groups.values()];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Element-wise equality of two normalised tag-value tuples. Compares on the SAME
|
|
158
|
+
* canonical form {@link childTableName} hashes with ({@link stringifyTagValue}),
|
|
159
|
+
* not by reference — otherwise two structurally-equal but distinct object/JSON
|
|
160
|
+
* `@Tag` references (which hash to the same child) would read as unequal and
|
|
161
|
+
* trip a spurious `E_EON_CHILD_COLLISION`, blocking a legitimate batch. A TRUE
|
|
162
|
+
* hash collision of genuinely distinct tag-sets still differs here and is caught.
|
|
163
|
+
*/
|
|
164
|
+
function sameTagValues(a: readonly unknown[], b: readonly unknown[]): boolean {
|
|
165
|
+
if (a.length !== b.length) return false;
|
|
166
|
+
for (let i = 0; i < a.length; i++) {
|
|
167
|
+
if (stringifyTagValue(a[i]) !== stringifyTagValue(b[i])) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Coerce a timestamp value to `bigint`, honouring the 58.1
|
|
174
|
+
* `E_EON_PARAM_PRECISION` boundary: a `number` that is not a safe integer has
|
|
175
|
+
* already lost precision, so it is rejected loud rather than bound corrupted.
|
|
176
|
+
*/
|
|
177
|
+
export function coerceTimestamp(value: unknown, property: string): bigint {
|
|
178
|
+
if (typeof value === "bigint") return value;
|
|
179
|
+
if (typeof value === "number") {
|
|
180
|
+
if (!Number.isInteger(value) || !Number.isSafeInteger(value)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`[E_EON_PARAM_PRECISION] timestamp '${property}' value ${value} is not a safe integer; pass nanosecond/BIGINT timestamps as bigint.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return BigInt(value);
|
|
186
|
+
}
|
|
187
|
+
throw new Error(
|
|
188
|
+
`[E_EON_TS_TYPE] timestamp '${property}' must be a bigint or a safe-integer number (got ${typeof value}).`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Inclusive [min, max] value bounds per signed-integer bind kind. */
|
|
193
|
+
export const INT_BOUNDS: Readonly<
|
|
194
|
+
Record<"tinyInt" | "smallInt" | "int" | "bigInt", readonly [bigint, bigint]>
|
|
195
|
+
> = {
|
|
196
|
+
tinyInt: [-128n, 127n],
|
|
197
|
+
smallInt: [-32768n, 32767n],
|
|
198
|
+
int: [-2147483648n, 2147483647n],
|
|
199
|
+
bigInt: [-9223372036854775808n, 9223372036854775807n],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** Largest finite magnitude representable in IEEE-754 binary32 (`FLOAT`). */
|
|
203
|
+
export const FLOAT32_MAX = 3.4028234663852886e38;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Validate a non-timestamp value against its declared bind kind, rejecting the
|
|
207
|
+
* silent-corruption inputs the connector setters would otherwise truncate or
|
|
208
|
+
* coerce: a fractional/NaN/Infinity/string into an integer column, an integer
|
|
209
|
+
* OUTSIDE the column's width (i8/i16/i32/i64) that would wrap, a non-finite or
|
|
210
|
+
* f32-overflowing float, a non-boolean into a BOOL, or an object/array into a
|
|
211
|
+
* scalar string/binary column. `null`/`undefined` are allowed and bound as NULL;
|
|
212
|
+
* timestamps are validated separately by {@link coerceTimestamp}; JSON columns
|
|
213
|
+
* legitimately carry objects and are left to the connector setter.
|
|
214
|
+
*/
|
|
215
|
+
export function assertColumnValue(
|
|
216
|
+
value: unknown,
|
|
217
|
+
kind: EonBindKind,
|
|
218
|
+
property: string,
|
|
219
|
+
): void {
|
|
220
|
+
if (value === null || value === undefined) return;
|
|
221
|
+
switch (kind) {
|
|
222
|
+
case "int":
|
|
223
|
+
case "smallInt":
|
|
224
|
+
case "tinyInt":
|
|
225
|
+
case "bigInt": {
|
|
226
|
+
let asBig: bigint;
|
|
227
|
+
if (typeof value === "bigint") {
|
|
228
|
+
asBig = value;
|
|
229
|
+
} else if (
|
|
230
|
+
typeof value === "number" &&
|
|
231
|
+
Number.isInteger(value) &&
|
|
232
|
+
Number.isSafeInteger(value)
|
|
233
|
+
) {
|
|
234
|
+
asBig = BigInt(value);
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`[E_EON_VALUE_TYPE] integer column '${property}' requires a safe-integer number or bigint (got ${typeof value} ${String(value)}).`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const [min, max] = INT_BOUNDS[kind];
|
|
241
|
+
if (asBig < min || asBig > max) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`[E_EON_VALUE_RANGE] ${kind} column '${property}' value ${String(value)} is outside the ${kind} range [${min}, ${max}]; the connector setter would truncate it silently.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
case "float":
|
|
249
|
+
case "double": {
|
|
250
|
+
const asNum =
|
|
251
|
+
typeof value === "bigint"
|
|
252
|
+
? Number(value)
|
|
253
|
+
: typeof value === "number"
|
|
254
|
+
? value
|
|
255
|
+
: undefined;
|
|
256
|
+
if (asNum === undefined || !Number.isFinite(asNum)) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`[E_EON_VALUE_TYPE] float column '${property}' requires a finite number (got ${typeof value} ${String(value)}).`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (kind === "float" && Math.abs(asNum) > FLOAT32_MAX) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`[E_EON_VALUE_RANGE] float (f32) column '${property}' value ${String(value)} exceeds the binary32 range and would be stored as Infinity; use a DOUBLE column.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
case "bool":
|
|
269
|
+
if (typeof value === "boolean") return;
|
|
270
|
+
throw new Error(
|
|
271
|
+
`[E_EON_VALUE_TYPE] bool column '${property}' requires a boolean (got ${typeof value} ${String(value)}).`,
|
|
272
|
+
);
|
|
273
|
+
case "varchar":
|
|
274
|
+
case "nchar":
|
|
275
|
+
case "varbinary":
|
|
276
|
+
// Mirror the schemaless path's object-reject: a non-scalar bound to a
|
|
277
|
+
// string/binary column stringifies to "[object Object]" — silent
|
|
278
|
+
// corruption. JSON columns (below, via default) legitimately take objects.
|
|
279
|
+
if (typeof value === "object") {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`[E_EON_VALUE_TYPE] string/binary column '${property}' requires a scalar value, not an object/array.`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
case "decimal":
|
|
286
|
+
// DECIMAL binds as a string/number to preserve precision; only a
|
|
287
|
+
// non-finite number (NaN/Infinity) is unrenderable — reject it.
|
|
288
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`[E_EON_VALUE_TYPE] decimal column '${property}' requires a finite number or a string (got ${String(value)}).`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
default:
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Build the columnar STMT ingest request from a plan + points: group by child,
|
|
301
|
+
* chunk at `batchSize`, and accumulate one SoA column per value column and one
|
|
302
|
+
* single-element column per tag. NO per-row object is produced.
|
|
303
|
+
*/
|
|
304
|
+
export function buildColumnarIngest(
|
|
305
|
+
plan: ColumnarPlan,
|
|
306
|
+
points: readonly IngestPoint[],
|
|
307
|
+
): EonColumnarIngest {
|
|
308
|
+
const children: EonChildBatch[] = [];
|
|
309
|
+
const tagProps = plan.tags.map((t) => t.property);
|
|
310
|
+
|
|
311
|
+
for (const group of groupByChild(plan.stable, tagProps, points)) {
|
|
312
|
+
for (let start = 0; start < group.rows.length; start += plan.batchSize) {
|
|
313
|
+
const slice = group.rows.slice(start, start + plan.batchSize);
|
|
314
|
+
const columns: EonBoundColumn[] = plan.columns.map((col) => ({
|
|
315
|
+
kind: col.kind,
|
|
316
|
+
values:
|
|
317
|
+
col.property === plan.tsProperty
|
|
318
|
+
? slice.map((pt) => coerceTimestamp(pt[col.property], col.property))
|
|
319
|
+
: slice.map((pt) => {
|
|
320
|
+
const value = pt[col.property] ?? null;
|
|
321
|
+
assertColumnValue(value, col.kind, col.property);
|
|
322
|
+
return value;
|
|
323
|
+
}),
|
|
324
|
+
}));
|
|
325
|
+
const tags: EonBoundColumn[] = plan.tags.map((tag, i) => {
|
|
326
|
+
const value = group.tagValues[i] ?? null;
|
|
327
|
+
assertColumnValue(value, tag.kind, tag.property);
|
|
328
|
+
return { kind: tag.kind, values: [value] };
|
|
329
|
+
});
|
|
330
|
+
children.push({ table: group.table, tags, columns });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { sql: plan.templateSql, children };
|
|
335
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Super-table metadata Symbols.
|
|
3
|
+
*
|
|
4
|
+
* Extracted into their own module — mirroring atlas `metadata-keys.ts` — so the
|
|
5
|
+
* decorator layer reads them without importing a base class, avoiding the
|
|
6
|
+
* runtime cycle that a shared `BaseEntity` import would form (atlas fallow
|
|
7
|
+
* 2026-06-14). Pure Symbols, zero runtime dependencies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The `{ name }` super-table descriptor (mirrors atlas `ENTITY_KEY`). */
|
|
11
|
+
export const SUPER_TABLE_KEY = Symbol("eon:superTable");
|
|
12
|
+
|
|
13
|
+
/** The ordered metric-column registry (`@Timestamp` + `@Column`). */
|
|
14
|
+
export const COLUMNS_KEY = Symbol("eon:columns");
|
|
15
|
+
|
|
16
|
+
/** The tag registry (`@Tag`) — separate from columns (no atlas analogue). */
|
|
17
|
+
export const TAGS_KEY = Symbol("eon:tags");
|
|
18
|
+
|
|
19
|
+
/** The property name of the mandatory first `TIMESTAMP` column (`@Timestamp`). */
|
|
20
|
+
export const TIMESTAMP_KEY = Symbol("eon:timestamp");
|