@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
package/src/index.ts ADDED
@@ -0,0 +1,118 @@
1
+ import "reflect-metadata";
2
+
3
+ export {
4
+ defineConfig,
5
+ type EonConfig,
6
+ type EonConnectionConfig,
7
+ } from "./connection/config.js";
8
+ export {
9
+ type EonBindKind,
10
+ type EonBoundColumn,
11
+ type EonChildBatch,
12
+ type EonColumnarIngest,
13
+ type EonConnection,
14
+ EonConnectionError,
15
+ type EonLineProtocol,
16
+ type EonSchemalessOptions,
17
+ type EonSchemalessPrecision,
18
+ } from "./connection/EonConnection.js";
19
+ export { connectWsEon } from "./connection/websocket.js";
20
+ export {
21
+ Column,
22
+ type EonColumnMetadata,
23
+ type EonColumnOptions,
24
+ type EonTagMetadata,
25
+ type EonTagOptions,
26
+ getColumnMetadata,
27
+ getSuperTableMetadata,
28
+ getTagMetadata,
29
+ getTimestampColumn,
30
+ SuperTable,
31
+ type SuperTableMetadata,
32
+ Tag,
33
+ Timestamp,
34
+ } from "./decorators/superTable.js";
35
+ export type {
36
+ EonAppContext,
37
+ EonConnector,
38
+ EonService,
39
+ } from "./EonProvider.js";
40
+ export { EonProvider } from "./EonProvider.js";
41
+ export { toLineProtocol } from "./ingest/schemaless.js";
42
+ export { buildLiteralInserts } from "./ingest/sql.js";
43
+ export {
44
+ buildColumnarIngest,
45
+ type ColumnarPlan,
46
+ compileStmtTemplate,
47
+ DEFAULT_BATCH_SIZE,
48
+ groupByChild,
49
+ type IngestPoint,
50
+ type PlanColumn,
51
+ toBindKind,
52
+ } from "./ingest/stmt.js";
53
+ export {
54
+ type CompiledStatement,
55
+ compileStatementNative,
56
+ type EonDialect,
57
+ quoteIdentNative,
58
+ } from "./query/native.js";
59
+ export {
60
+ type EonFillMode,
61
+ type EonFunctionSelect,
62
+ type EonOrderDirection,
63
+ type EonPseudoSelect,
64
+ type EonScalar,
65
+ type EonSelectItem,
66
+ type EonWhereOperator,
67
+ type EonWhereValue,
68
+ TimeSeriesQuery,
69
+ } from "./query/TimeSeriesQuery.js";
70
+ export {
71
+ SuperTableRepository,
72
+ type SuperTableRepositoryOptions,
73
+ } from "./repository/SuperTableRepository.js";
74
+ export {
75
+ type AlterChange,
76
+ type AlterDatabaseSpec,
77
+ type AlterStableSpec,
78
+ type CreateChildTableSpec,
79
+ type CreateDatabaseSpec,
80
+ type CreateStableSpec,
81
+ type CreateTableSpec,
82
+ type DropStableSpec,
83
+ type DropTableSpec,
84
+ type EonAlterDatabaseOptions,
85
+ type EonCacheModel,
86
+ type EonColumnSpec,
87
+ type EonDatabaseOptions,
88
+ type EonLogicalType,
89
+ type EonPrecision,
90
+ TYPE_KIND_MAP,
91
+ } from "./schema/CreateStableSpec.js";
92
+ export {
93
+ compileCreateStableSpec,
94
+ requireSuperTableName,
95
+ type SuperTableClass,
96
+ } from "./schema/compile.js";
97
+ export {
98
+ type EonMigrationOptions,
99
+ EonMigrationRunner,
100
+ type MigrationState,
101
+ type MigrationStatus,
102
+ } from "./schema/EonMigrationRunner.js";
103
+ export {
104
+ AlterStableBuilder,
105
+ type AlterTypeSetter,
106
+ BasicTableBuilder,
107
+ EonSchema,
108
+ StableBuilder,
109
+ type StableColumnHandle,
110
+ } from "./schema/EonSchema.js";
111
+ export { Migration } from "./schema/Migration.js";
112
+ export {
113
+ type CreateChildTableOptions,
114
+ childTableName,
115
+ createChildTable,
116
+ dropSuperTable,
117
+ syncSuperTable,
118
+ } from "./schema/sync.js";
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Schemaless line-protocol rendering (story 58.4, AC4).
3
+ *
4
+ * Renders points to InfluxDB line-protocol strings for
5
+ * `EonConnection.schemaless`. This is NOT SQL — it is InfluxDB's line format, so
6
+ * it is rendered in TS (no compiler seam), with the format's own escaping rules
7
+ * applied to the measurement, tag keys/values, and field keys, and string field
8
+ * values double-quoted (context7 `/taosdata/tdengine`, InfluxDB line protocol).
9
+ *
10
+ * Schemaless is a documented ~8–10× slower helper than the STMT path and must
11
+ * NOT be used as the default bulk path (AD5 / AC4).
12
+ */
13
+
14
+ import type { EonBindKind } from "../connection/EonConnection.js";
15
+ import type { ColumnarPlan, IngestPoint } from "./stmt.js";
16
+ import { coerceTimestamp, FLOAT32_MAX, INT_BOUNDS } from "./stmt.js";
17
+
18
+ /**
19
+ * Reject any control character (newline, CR, tab, NUL, …). Line protocol is
20
+ * newline-delimited and cannot encode a control byte in ANY position, so
21
+ * backslash-escaping does not neutralize it — a raw newline in a caller tag or
22
+ * field value would terminate the line and inject a forged point. These bytes
23
+ * must be refused, not escaped (memory `feedback_security_first`).
24
+ */
25
+ function assertNoControlChar(value: string, role: string): void {
26
+ for (let i = 0; i < value.length; i++) {
27
+ const code = value.charCodeAt(i);
28
+ if (code <= 0x1f || code === 0x7f) {
29
+ throw new Error(
30
+ `[E_EON_SCHEMALESS_CONTROL_CHAR] ${role} contains a control character (code ${code}); line protocol cannot encode it.`,
31
+ );
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Escape a measurement name: comma, space, and backslash. Backslash is escaped
38
+ * too, else a trailing `\` would escape the following delimiter and desync the
39
+ * parser. Control chars are rejected upstream (so no `\s`).
40
+ */
41
+ function escapeMeasurement(value: string): string {
42
+ assertNoControlChar(value, "measurement");
43
+ return value.replace(/[\\, ]/g, "\\$&");
44
+ }
45
+
46
+ /** Escape a tag key/value or field key: comma, equals, space, and backslash. */
47
+ function escapeKeyOrTag(value: string): string {
48
+ assertNoControlChar(value, "tag/field key or value");
49
+ return value.replace(/[\\,= ]/g, "\\$&");
50
+ }
51
+
52
+ /** Escape a string field value: backslash and double-quote (then wrap in `"`). */
53
+ function quoteStringField(value: string): string {
54
+ assertNoControlChar(value, "string field value");
55
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
56
+ }
57
+
58
+ /**
59
+ * Render one field value into its line-protocol form, typed by its bind kind.
60
+ * Numeric kinds are validated to the same precision/finiteness contract the STMT
61
+ * path enforces (`E_EON_PARAM_PRECISION`): an unsafe-integer `number`, a
62
+ * non-finite float, or a non-numeric value is rejected loud rather than emitted
63
+ * as a lossy/invalid token (e.g. `1e+21i`, `NaNi`) that silently corrupts data.
64
+ */
65
+ function renderFieldValue(
66
+ kind: EonBindKind,
67
+ value: unknown,
68
+ property: string,
69
+ ): string {
70
+ switch (kind) {
71
+ case "int":
72
+ case "bigInt":
73
+ case "smallInt":
74
+ case "tinyInt": {
75
+ // Integer field: the `i` suffix. bigint is exact; a `number` must be a
76
+ // safe integer or it has already lost precision. The value must also fit
77
+ // the column width — the SQL path's i64 guard (`stringifyPrecisionSafe`)
78
+ // never runs on the schemaless path, so bound it here or an out-of-range
79
+ // value renders as an invalid/overflowing token (`999...i`).
80
+ let asBig: bigint;
81
+ if (typeof value === "bigint") {
82
+ asBig = value;
83
+ } else if (
84
+ typeof value === "number" &&
85
+ Number.isInteger(value) &&
86
+ Number.isSafeInteger(value)
87
+ ) {
88
+ asBig = BigInt(value);
89
+ } else {
90
+ throw new Error(
91
+ `[E_EON_PARAM_PRECISION] integer field '${property}' value ${String(value)} is not a safe integer; pass i64 values as bigint.`,
92
+ );
93
+ }
94
+ const [min, max] = INT_BOUNDS[kind];
95
+ if (asBig < min || asBig > max) {
96
+ throw new Error(
97
+ `[E_EON_SCHEMALESS_FIELD] ${kind} field '${property}' value ${String(value)} is outside the ${kind} range [${min}, ${max}].`,
98
+ );
99
+ }
100
+ return `${asBig}i`;
101
+ }
102
+ case "float":
103
+ case "double":
104
+ case "decimal": {
105
+ const asNum =
106
+ typeof value === "bigint"
107
+ ? Number(value)
108
+ : typeof value === "number"
109
+ ? value
110
+ : undefined;
111
+ if (asNum === undefined || !Number.isFinite(asNum)) {
112
+ throw new Error(
113
+ `[E_EON_SCHEMALESS_FIELD] numeric field '${property}' value ${String(value)} is not a finite number.`,
114
+ );
115
+ }
116
+ if (kind === "float" && Math.abs(asNum) > FLOAT32_MAX) {
117
+ throw new Error(
118
+ `[E_EON_SCHEMALESS_FIELD] float (f32) field '${property}' value ${String(value)} exceeds the binary32 range and would render as Infinity.`,
119
+ );
120
+ }
121
+ return `${value}`;
122
+ }
123
+ case "bool":
124
+ if (typeof value !== "boolean") {
125
+ throw new Error(
126
+ `[E_EON_SCHEMALESS_FIELD] bool field '${property}' requires a boolean (got ${typeof value}).`,
127
+ );
128
+ }
129
+ return value ? "true" : "false";
130
+ default:
131
+ // varchar / nchar / varbinary / json / timestamp → quoted string.
132
+ // A non-scalar (object/array) would stringify to "[object Object]" /
133
+ // "a,b" — silent corruption. Reject loud rather than emit garbage.
134
+ if (typeof value === "object") {
135
+ throw new Error(
136
+ `[E_EON_SCHEMALESS_FIELD] string field '${property}' requires a scalar value, not an object/array.`,
137
+ );
138
+ }
139
+ return quoteStringField(String(value));
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Render points to InfluxDB line-protocol lines. The measurement is the
145
+ * super-table name, tags come from the entity's `@Tag` columns, and every
146
+ * present metric column (excluding the timestamp) becomes a field. Points with
147
+ * no fields are skipped (line protocol requires ≥1 field) — reported by the
148
+ * caller, never silently emitted as an invalid line.
149
+ */
150
+ export function toLineProtocol(
151
+ plan: ColumnarPlan,
152
+ points: readonly IngestPoint[],
153
+ ): string[] {
154
+ if (plan.stable === "") {
155
+ throw new Error(
156
+ "[E_EON_SCHEMALESS_MEASUREMENT] super-table (measurement) name is empty; cannot render a line-protocol point.",
157
+ );
158
+ }
159
+ const measurement = escapeMeasurement(plan.stable);
160
+ const lines: string[] = [];
161
+
162
+ for (const point of points) {
163
+ const tagParts = plan.tags
164
+ .filter((tag) => point[tag.property] != null)
165
+ .map((tag) => {
166
+ const value = point[tag.property];
167
+ // Tags are not type-checked like fields; a non-scalar (object/array)
168
+ // would stringify to "[object Object]" / "a,b" — silent corruption.
169
+ if (typeof value === "object") {
170
+ throw new Error(
171
+ `[E_EON_SCHEMALESS_TAG] tag '${tag.property}' requires a scalar value, not an object/array.`,
172
+ );
173
+ }
174
+ const raw = String(value);
175
+ // Line protocol forbids an empty tag value (`location=` is invalid and
176
+ // TDengine rejects the whole batch). Reject loud rather than emit a
177
+ // malformed token — a control char in `raw` is caught by escapeKeyOrTag.
178
+ if (raw === "") {
179
+ throw new Error(
180
+ `[E_EON_SCHEMALESS_EMPTY_TAG] tag '${tag.property}' is an empty string; line protocol requires a non-empty tag value.`,
181
+ );
182
+ }
183
+ return `${escapeKeyOrTag(tag.property)}=${escapeKeyOrTag(raw)}`;
184
+ });
185
+
186
+ const fieldParts = plan.columns
187
+ .filter(
188
+ (col) =>
189
+ col.property !== plan.tsProperty && point[col.property] != null,
190
+ )
191
+ .map(
192
+ (col) =>
193
+ `${escapeKeyOrTag(col.property)}=${renderFieldValue(col.kind, point[col.property], col.property)}`,
194
+ );
195
+
196
+ if (fieldParts.length === 0) {
197
+ throw new Error(
198
+ `[E_EON_SCHEMALESS_NO_FIELDS] point for '${plan.stable}' has no metric fields; line protocol requires at least one field.`,
199
+ );
200
+ }
201
+
202
+ const ts = coerceTimestamp(point[plan.tsProperty], plan.tsProperty);
203
+ const prefix =
204
+ tagParts.length > 0
205
+ ? `${measurement},${tagParts.join(",")}`
206
+ : measurement;
207
+ lines.push(`${prefix} ${fieldParts.join(",")} ${ts}`);
208
+ }
209
+
210
+ return lines;
211
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Literal SQL INSERT ingest planning (story 58.4, AC5) — the convenience /
3
+ * fallback path that runs through the literal-only `EonConnection.exec`.
4
+ *
5
+ * Because `exec` takes literal SQL (no bind params, 58.2 D3), the value literals
6
+ * are rendered by the **Rust compiler's `literal: true` INSERT mode** (D6, OQ1),
7
+ * reusing the single `render_literal` injection seam. This module NEVER
8
+ * string-builds SQL or interpolates a value — it assembles a JSON `InsertSpec`
9
+ * and hands it to `compileStatementNative`. It groups points by child table
10
+ * (the `USING … TAGS` auto-create form) exactly like the STMT path.
11
+ */
12
+
13
+ import { compileStatementNative } from "../query/native.js";
14
+ import type { ColumnarPlan, IngestPoint } from "./stmt.js";
15
+ import { assertColumnValue, coerceTimestamp, groupByChild } from "./stmt.js";
16
+
17
+ /**
18
+ * Build one literal `INSERT … USING <stable> TAGS(...) VALUES(...)` statement per
19
+ * child table. Every value is validated against its declared bind kind up front —
20
+ * the timestamp through `coerceTimestamp` (rejecting fractional/string/unsafe
21
+ * inputs the compile boundary alone would pass, e.g. `1700000000000.5` or a date
22
+ * string), other columns through `assertColumnValue` — so the literal path is
23
+ * exactly as precision- and type-safe as the STMT path, and the Rust compiler
24
+ * renders only the vetted literals (never TS interpolation).
25
+ */
26
+ export function buildLiteralInserts(
27
+ plan: ColumnarPlan,
28
+ points: readonly IngestPoint[],
29
+ ): string[] {
30
+ const columnNames = plan.columns.map((c) => c.property);
31
+ const tagProps = plan.tags.map((t) => t.property);
32
+ const statements: string[] = [];
33
+
34
+ for (const group of groupByChild(plan.stable, tagProps, points)) {
35
+ const rows = group.rows.map((point) =>
36
+ plan.columns.map((col) => {
37
+ if (col.property === plan.tsProperty) {
38
+ return coerceTimestamp(point[col.property], col.property);
39
+ }
40
+ const value = point[col.property] ?? null;
41
+ assertColumnValue(value, col.kind, col.property);
42
+ return value;
43
+ }),
44
+ );
45
+ const compiled = compileStatementNative(
46
+ {
47
+ kind: "insert",
48
+ table: group.table,
49
+ using: plan.stable,
50
+ tags: group.tagValues.map((v, i) => {
51
+ const value = v ?? null;
52
+ const tag = plan.tags[i];
53
+ if (tag !== undefined)
54
+ assertColumnValue(value, tag.kind, tag.property);
55
+ return value;
56
+ }),
57
+ columns: columnNames,
58
+ rows,
59
+ literal: true,
60
+ },
61
+ "tdengine",
62
+ );
63
+ const [sql, ...rest] = compiled.statements;
64
+ if (sql === undefined || rest.length > 0) {
65
+ throw new Error(
66
+ `[E_EON_LITERAL_INSERT] expected exactly one statement for child '${group.table}', got ${compiled.statements.length}`,
67
+ );
68
+ }
69
+ statements.push(sql);
70
+ }
71
+
72
+ return statements;
73
+ }