@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.
Files changed (123) hide show
  1. package/README.md +69 -0
  2. package/dist/EonProvider.d.ts +51 -0
  3. package/dist/EonProvider.d.ts.map +1 -0
  4. package/dist/EonProvider.js +154 -0
  5. package/dist/EonProvider.js.map +1 -0
  6. package/dist/connection/EonConnection.d.ts +116 -0
  7. package/dist/connection/EonConnection.d.ts.map +1 -0
  8. package/dist/connection/EonConnection.js +15 -0
  9. package/dist/connection/EonConnection.js.map +1 -0
  10. package/dist/connection/config.d.ts +56 -0
  11. package/dist/connection/config.d.ts.map +1 -0
  12. package/dist/connection/config.js +14 -0
  13. package/dist/connection/config.js.map +1 -0
  14. package/dist/connection/websocket.d.ts +16 -0
  15. package/dist/connection/websocket.d.ts.map +1 -0
  16. package/dist/connection/websocket.js +383 -0
  17. package/dist/connection/websocket.js.map +1 -0
  18. package/dist/decorators/superTable.d.ts +73 -0
  19. package/dist/decorators/superTable.d.ts.map +1 -0
  20. package/dist/decorators/superTable.js +98 -0
  21. package/dist/decorators/superTable.js.map +1 -0
  22. package/dist/index.d.ts +20 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +19 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ingest/schemaless.d.ts +22 -0
  27. package/dist/ingest/schemaless.d.ts.map +1 -0
  28. package/dist/ingest/schemaless.js +161 -0
  29. package/dist/ingest/schemaless.js.map +1 -0
  30. package/dist/ingest/sql.d.ts +23 -0
  31. package/dist/ingest/sql.d.ts.map +1 -0
  32. package/dist/ingest/sql.js +59 -0
  33. package/dist/ingest/sql.js.map +1 -0
  34. package/dist/ingest/stmt.d.ts +82 -0
  35. package/dist/ingest/stmt.d.ts.map +1 -0
  36. package/dist/ingest/stmt.js +246 -0
  37. package/dist/ingest/stmt.js.map +1 -0
  38. package/dist/metadata-keys.d.ts +17 -0
  39. package/dist/metadata-keys.d.ts.map +1 -0
  40. package/dist/metadata-keys.js +17 -0
  41. package/dist/metadata-keys.js.map +1 -0
  42. package/dist/query/TimeSeriesQuery.d.ts +121 -0
  43. package/dist/query/TimeSeriesQuery.d.ts.map +1 -0
  44. package/dist/query/TimeSeriesQuery.js +291 -0
  45. package/dist/query/TimeSeriesQuery.js.map +1 -0
  46. package/dist/query/native.d.ts +22 -0
  47. package/dist/query/native.d.ts.map +1 -0
  48. package/dist/query/native.js +135 -0
  49. package/dist/query/native.js.map +1 -0
  50. package/dist/repository/SuperTableRepository.d.ts +80 -0
  51. package/dist/repository/SuperTableRepository.d.ts.map +1 -0
  52. package/dist/repository/SuperTableRepository.js +232 -0
  53. package/dist/repository/SuperTableRepository.js.map +1 -0
  54. package/dist/schema/CreateStableSpec.d.ts +140 -0
  55. package/dist/schema/CreateStableSpec.d.ts.map +1 -0
  56. package/dist/schema/CreateStableSpec.js +41 -0
  57. package/dist/schema/CreateStableSpec.js.map +1 -0
  58. package/dist/schema/EonMigrationRunner.d.ts +71 -0
  59. package/dist/schema/EonMigrationRunner.d.ts.map +1 -0
  60. package/dist/schema/EonMigrationRunner.js +295 -0
  61. package/dist/schema/EonMigrationRunner.js.map +1 -0
  62. package/dist/schema/EonSchema.d.ts +128 -0
  63. package/dist/schema/EonSchema.d.ts.map +1 -0
  64. package/dist/schema/EonSchema.js +309 -0
  65. package/dist/schema/EonSchema.js.map +1 -0
  66. package/dist/schema/Migration.d.ts +47 -0
  67. package/dist/schema/Migration.d.ts.map +1 -0
  68. package/dist/schema/Migration.js +55 -0
  69. package/dist/schema/Migration.js.map +1 -0
  70. package/dist/schema/compile.d.ts +33 -0
  71. package/dist/schema/compile.d.ts.map +1 -0
  72. package/dist/schema/compile.js +70 -0
  73. package/dist/schema/compile.js.map +1 -0
  74. package/dist/schema/sync.d.ts +45 -0
  75. package/dist/schema/sync.d.ts.map +1 -0
  76. package/dist/schema/sync.js +102 -0
  77. package/dist/schema/sync.js.map +1 -0
  78. package/dist/services/connection.d.ts +33 -0
  79. package/dist/services/connection.d.ts.map +1 -0
  80. package/dist/services/connection.js +71 -0
  81. package/dist/services/connection.js.map +1 -0
  82. package/dist/testing/Factory.d.ts +43 -0
  83. package/dist/testing/Factory.d.ts.map +1 -0
  84. package/dist/testing/Factory.js +134 -0
  85. package/dist/testing/Factory.js.map +1 -0
  86. package/dist/testing/FakeEonConnection.d.ts +43 -0
  87. package/dist/testing/FakeEonConnection.d.ts.map +1 -0
  88. package/dist/testing/FakeEonConnection.js +315 -0
  89. package/dist/testing/FakeEonConnection.js.map +1 -0
  90. package/dist/testing/index.d.ts +34 -0
  91. package/dist/testing/index.d.ts.map +1 -0
  92. package/dist/testing/index.js +67 -0
  93. package/dist/testing/index.js.map +1 -0
  94. package/index.darwin-arm64.node +0 -0
  95. package/index.darwin-x64.node +0 -0
  96. package/index.linux-arm64-gnu.node +0 -0
  97. package/index.linux-x64-gnu.node +0 -0
  98. package/index.win32-x64-msvc.node +0 -0
  99. package/package.json +65 -0
  100. package/scripts/copy-napi.mjs +72 -0
  101. package/src/EonProvider.ts +222 -0
  102. package/src/connection/EonConnection.ts +146 -0
  103. package/src/connection/config.ts +60 -0
  104. package/src/connection/websocket.ts +436 -0
  105. package/src/decorators/superTable.ts +169 -0
  106. package/src/index.ts +118 -0
  107. package/src/ingest/schemaless.ts +211 -0
  108. package/src/ingest/sql.ts +73 -0
  109. package/src/ingest/stmt.ts +335 -0
  110. package/src/metadata-keys.ts +20 -0
  111. package/src/query/TimeSeriesQuery.ts +480 -0
  112. package/src/query/native.ts +180 -0
  113. package/src/repository/SuperTableRepository.ts +347 -0
  114. package/src/schema/CreateStableSpec.ts +184 -0
  115. package/src/schema/EonMigrationRunner.ts +378 -0
  116. package/src/schema/EonSchema.ts +396 -0
  117. package/src/schema/Migration.ts +66 -0
  118. package/src/schema/compile.ts +109 -0
  119. package/src/schema/sync.ts +142 -0
  120. package/src/services/connection.ts +89 -0
  121. package/src/testing/Factory.ts +186 -0
  122. package/src/testing/FakeEonConnection.ts +351 -0
  123. 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");