@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,347 @@
1
+ /**
2
+ * `SuperTableRepository<TPoint>` — the Atlas-shaped ingest API for a TDengine
3
+ * super-table (story 58.4).
4
+ *
5
+ * Mirrors `@c9up/atlas` `BaseRepository` naming: `ingest` (≙ `create`) and
6
+ * `ingestMany` (≙ `createMany`, the bulk primitive), plus two extra write paths
7
+ * TDengine adds — `ingestSql` (literal INSERT) and `ingestSchemaless` (line
8
+ * protocol). It deliberately OMITS everything TDengine has no analogue for
9
+ * (AC8): no `save`/`upsert`/`firstOrCreate`, no `useTransaction`/BEGIN-COMMIT,
10
+ * no `RETURNING`, no DB-generated-PK hydrate-back. TDengine writes are
11
+ * append / last-write-wins by the caller-supplied timestamp.
12
+ *
13
+ * Schema is read ONLY through the 58.3 decorator metadata getters — never
14
+ * `key in instance` (the `@Column() declare x` pitfall, memory
15
+ * `project_atlas_declare_hydration`). The connection is injected structurally
16
+ * (agnostic leaf, AD1 — no `@c9up/ream` import).
17
+ */
18
+
19
+ import type {
20
+ EonConnection,
21
+ EonSchemalessOptions,
22
+ } from "../connection/EonConnection.js";
23
+ import {
24
+ getColumnMetadata,
25
+ getSuperTableMetadata,
26
+ getTagMetadata,
27
+ getTimestampColumn,
28
+ } from "../decorators/superTable.js";
29
+ import { toLineProtocol } from "../ingest/schemaless.js";
30
+ import { buildLiteralInserts } from "../ingest/sql.js";
31
+ import {
32
+ buildColumnarIngest,
33
+ type ColumnarPlan,
34
+ compileStmtTemplate,
35
+ DEFAULT_BATCH_SIZE,
36
+ type IngestPoint,
37
+ type PlanColumn,
38
+ toBindKind,
39
+ } from "../ingest/stmt.js";
40
+ import { TimeSeriesQuery } from "../query/TimeSeriesQuery.js";
41
+ import {
42
+ orderTimestampFirst,
43
+ type SuperTableClass,
44
+ } from "../schema/compile.js";
45
+
46
+ /** Options for {@link SuperTableRepository}. */
47
+ export interface SuperTableRepositoryOptions {
48
+ /** STMT chunk size (rows per child per bind). Default {@link DEFAULT_BATCH_SIZE}. */
49
+ readonly batchSize?: number;
50
+ }
51
+
52
+ /**
53
+ * Repository for a TDengine super-table — atlas `BaseRepository` parity for the
54
+ * time-series model. NAMED deviation from atlas (prime directive: name any
55
+ * divergence): atlas types reads by constructing `new Entity()` instances; eon
56
+ * reads plain records and never constructs instances, so typed reads are opt-in
57
+ * through `query(mapPoint)` — the caller supplies the row → `E` mapper, composed
58
+ * on top of the bigint/timestamp-reviving base hydrator (no `as`, the decode
59
+ * stays honest). Called without a mapper, points stay `Record<string, unknown>`.
60
+ */
61
+ export class SuperTableRepository {
62
+ readonly #conn: EonConnection;
63
+ readonly #plan: ColumnarPlan;
64
+ readonly #querySchema: QuerySchema;
65
+
66
+ constructor(
67
+ entityClass: SuperTableClass,
68
+ conn: EonConnection,
69
+ options?: SuperTableRepositoryOptions,
70
+ ) {
71
+ // Fail loud on a null/undefined connection — almost always a failed IoC
72
+ // injection (atlas `BaseRepository.ts:245` parity), not a runtime NPE later.
73
+ if (conn === null || conn === undefined) {
74
+ throw new Error(
75
+ `[E_EON_MISSING_CONNECTION] SuperTableRepository for '${entityClass?.name ?? "<unknown entity>"}' requires an EonConnection (got ${conn === null ? "null" : "undefined"}). Check IoC constructor injection is wired.`,
76
+ );
77
+ }
78
+ this.#conn = conn;
79
+ this.#plan = buildPlan(entityClass, options);
80
+ this.#querySchema = buildQuerySchema(entityClass);
81
+ }
82
+
83
+ /**
84
+ * A fluent time-series read builder bound to this repository's connection and
85
+ * the entity's 58.3 schema metadata. Rows hydrate through a metadata-driven
86
+ * closure — declared columns by their `@Column`/`@Tag` metadata (bigint /
87
+ * timestamp revived as `bigint`), NEVER `key in instance` (the `declare`-field
88
+ * pitfall), and window pseudo-columns / aggregate aliases attached raw.
89
+ *
90
+ * Pass `mapPoint` to project each decoded row into a typed `E` (atlas-parity
91
+ * typed reads, no `as`): the mapper composes on top of the base hydrator so
92
+ * bigint/timestamp revival still happens first. Without a mapper, points stay
93
+ * `Record<string, unknown>`.
94
+ */
95
+ query(): TimeSeriesQuery<Record<string, unknown>>;
96
+ query<E extends object>(
97
+ mapPoint: (row: Record<string, unknown>) => E,
98
+ ): TimeSeriesQuery<E>;
99
+ query<E extends object>(
100
+ mapPoint?: (row: Record<string, unknown>) => E,
101
+ ): TimeSeriesQuery<E> | TimeSeriesQuery<Record<string, unknown>> {
102
+ const base = this.#querySchema.hydrate;
103
+ if (mapPoint) {
104
+ return new TimeSeriesQuery(
105
+ this.#querySchema.stable,
106
+ this.#conn,
107
+ (row) => mapPoint(base(row)),
108
+ this.#querySchema.known,
109
+ );
110
+ }
111
+ return new TimeSeriesQuery(
112
+ this.#querySchema.stable,
113
+ this.#conn,
114
+ base,
115
+ this.#querySchema.known,
116
+ );
117
+ }
118
+
119
+ /** Ingest a single point (≙ atlas `create`). */
120
+ async ingest(point: IngestPoint): Promise<void> {
121
+ await this.ingestMany([point]);
122
+ }
123
+
124
+ /**
125
+ * Bulk ingest via the columnar STMT path (≙ atlas `createMany`) — the default
126
+ * high-throughput, injection-safe write. Groups by child table, binds SoA
127
+ * columns once per batch. Returns total rows affected. `async` so a plan
128
+ * error (e.g. an unsafe-integer timestamp) surfaces as a rejection, not a
129
+ * synchronous throw at the call site.
130
+ */
131
+ async ingestMany(
132
+ points: readonly IngestPoint[],
133
+ ): Promise<{ rowsAffected: number }> {
134
+ if (points.length === 0) return { rowsAffected: 0 };
135
+ return this.#conn.ingestColumnar(buildColumnarIngest(this.#plan, points));
136
+ }
137
+
138
+ /**
139
+ * Bulk ingest via literal SQL INSERT (AC5) — a convenience/fallback path run
140
+ * through `exec`. One `INSERT … USING … TAGS … VALUES` per child table; all
141
+ * value literals are rendered by the Rust compiler (no TS interpolation).
142
+ */
143
+ async ingestSql(
144
+ points: readonly IngestPoint[],
145
+ ): Promise<{ rowsAffected: number }> {
146
+ if (points.length === 0) return { rowsAffected: 0 };
147
+ let rowsAffected = 0;
148
+ for (const sql of buildLiteralInserts(this.#plan, points)) {
149
+ const result = await this.#conn.exec(sql);
150
+ rowsAffected += result.rowsAffected;
151
+ }
152
+ return { rowsAffected };
153
+ }
154
+
155
+ /**
156
+ * Bulk ingest via schemaless line protocol (AC4) — a documented ~8–10× slower
157
+ * helper, NOT the default bulk path. Renders points to InfluxDB line protocol
158
+ * from the entity metadata and passes them to the transport.
159
+ */
160
+ async ingestSchemaless(
161
+ points: readonly IngestPoint[],
162
+ options?: EonSchemalessOptions,
163
+ ): Promise<void> {
164
+ if (points.length === 0) return;
165
+ await this.#conn.schemaless(toLineProtocol(this.#plan, points), options);
166
+ }
167
+ }
168
+
169
+ /** Resolve the immutable ingest plan from a decorated super-table class. */
170
+ function buildPlan(
171
+ entityClass: SuperTableClass,
172
+ options?: SuperTableRepositoryOptions,
173
+ ): ColumnarPlan {
174
+ const meta = getSuperTableMetadata(entityClass);
175
+ if (!meta) {
176
+ throw new Error(
177
+ `[E_EON_NOT_A_SUPERTABLE] ${entityClass.name} is not decorated with @SuperTable`,
178
+ );
179
+ }
180
+ const tsProperty = getTimestampColumn(entityClass);
181
+ if (!tsProperty) {
182
+ throw new Error(
183
+ `[E_EON_NO_TIMESTAMP] super-table '${meta.name}' has no @Timestamp column`,
184
+ );
185
+ }
186
+
187
+ const columnMeta = getColumnMetadata(entityClass);
188
+ const tsColumn = columnMeta.find((c) => c.propertyKey === tsProperty);
189
+ if (!tsColumn) {
190
+ throw new Error(
191
+ `[E_EON_NO_TIMESTAMP] super-table '${meta.name}' @Timestamp column '${tsProperty}' is not registered`,
192
+ );
193
+ }
194
+ // Timestamp first, from the SAME shared ordering the STABLE was created with
195
+ // (`orderTimestampFirst`), so the positional STMT bind lines up with the
196
+ // STABLE's physical columns and can never drift.
197
+ const ordered = orderTimestampFirst(columnMeta, tsColumn, tsProperty);
198
+ const columns: PlanColumn[] = ordered.map((c) => ({
199
+ property: c.propertyKey,
200
+ kind: toBindKind(c.type, c.propertyKey),
201
+ }));
202
+
203
+ const tags: PlanColumn[] = getTagMetadata(entityClass).map((t) => ({
204
+ property: t.propertyKey,
205
+ kind: toBindKind(t.type, t.propertyKey),
206
+ }));
207
+ if (tags.length === 0) {
208
+ throw new Error(
209
+ `[E_EON_NO_TAGS] super-table '${meta.name}' has no @Tag columns; a child table cannot be routed without tags`,
210
+ );
211
+ }
212
+
213
+ const batchSize = options?.batchSize ?? DEFAULT_BATCH_SIZE;
214
+ if (!Number.isInteger(batchSize) || batchSize <= 0) {
215
+ throw new Error(
216
+ `[E_EON_BATCH_SIZE] batchSize must be a positive integer (got ${batchSize})`,
217
+ );
218
+ }
219
+
220
+ const templateSql = compileStmtTemplate(
221
+ meta.name,
222
+ tags.map((t) => t.property),
223
+ columns.map((c) => c.property),
224
+ );
225
+
226
+ return {
227
+ stable: meta.name,
228
+ templateSql,
229
+ tsProperty,
230
+ columns,
231
+ tags,
232
+ batchSize,
233
+ };
234
+ }
235
+
236
+ /** The metadata a {@link TimeSeriesQuery} needs to hydrate rows into points. */
237
+ interface QuerySchema {
238
+ /** The super-table name (FROM target). */
239
+ readonly stable: string;
240
+ /** Revive declared columns (bigint/timestamp → `bigint`); skip everything else. */
241
+ readonly hydrate: (row: Record<string, unknown>) => Record<string, unknown>;
242
+ /** Declared column + tag property names (used to separate raw window extras). */
243
+ readonly known: ReadonlySet<string>;
244
+ }
245
+
246
+ /**
247
+ * Build the read-side schema from the 58.3 decorator getters. The `@Column`/
248
+ * `@Tag` metadata is the ONLY source of truth — never `key in instance` (the
249
+ * `@Column() declare x` pitfall, memory `project_atlas_declare_hydration`).
250
+ */
251
+ function buildQuerySchema(entityClass: SuperTableClass): QuerySchema {
252
+ const meta = getSuperTableMetadata(entityClass);
253
+ if (!meta) {
254
+ throw new Error(
255
+ `[E_EON_NOT_A_SUPERTABLE] ${entityClass.name} is not decorated with @SuperTable`,
256
+ );
257
+ }
258
+ const tsProperty = getTimestampColumn(entityClass);
259
+ if (!tsProperty) {
260
+ throw new Error(
261
+ `[E_EON_NO_TIMESTAMP] super-table '${meta.name}' has no @Timestamp column`,
262
+ );
263
+ }
264
+
265
+ const columns = getColumnMetadata(entityClass);
266
+ const tags = getTagMetadata(entityClass);
267
+ const known = new Set<string>([
268
+ ...columns.map((c) => c.propertyKey),
269
+ ...tags.map((t) => t.propertyKey),
270
+ ]);
271
+ // A column reads back as `bigint` when it is the timestamp primary column or
272
+ // its logical type is a 64-bit integer — parity with the compile-boundary
273
+ // precision guard, so a nanosecond `ts` or a BIGINT metric never narrows to a
274
+ // lossy double.
275
+ const bigintColumns = new Set<string>();
276
+ for (const column of columns) {
277
+ if (column.propertyKey === tsProperty || isBigintLogicalType(column.type)) {
278
+ bigintColumns.add(column.propertyKey);
279
+ }
280
+ }
281
+ for (const tag of tags) {
282
+ if (isBigintLogicalType(tag.type)) bigintColumns.add(tag.propertyKey);
283
+ }
284
+
285
+ const hydrate = (row: Record<string, unknown>): Record<string, unknown> => {
286
+ // Decode by metadata membership, iterating the raw row's own keys — never
287
+ // `for (key in instance)` (a `declare` field is not an own property).
288
+ const point: Record<string, unknown> = {};
289
+ for (const [key, value] of Object.entries(row)) {
290
+ if (!known.has(key)) continue;
291
+ // `defineProperty`, not assignment: a column named `__proto__`/`constructor`
292
+ // would otherwise hit the inherited accessor and re-parent `point`.
293
+ Object.defineProperty(point, key, {
294
+ value: bigintColumns.has(key) ? reviveBigInt(value) : value,
295
+ writable: true,
296
+ enumerable: true,
297
+ configurable: true,
298
+ });
299
+ }
300
+ return point;
301
+ };
302
+
303
+ return { stable: meta.name, hydrate, known };
304
+ }
305
+
306
+ /**
307
+ * Logical types that read back as 64-bit integers. Matched case-insensitively
308
+ * and including the `bigInteger` alias, so it stays in lock-step with the DDL /
309
+ * bind side (`TYPE_KIND_MAP` accepts `bigint`/`bigInteger`/any-case `BIGINT`) —
310
+ * a divergence here would read a genuine BIGINT column back as a lossy `number`.
311
+ */
312
+ function isBigintLogicalType(type: string | undefined): boolean {
313
+ if (type === undefined) return false;
314
+ const t = type.toLowerCase();
315
+ return t === "bigint" || t === "biginteger" || t === "timestamp";
316
+ }
317
+
318
+ /**
319
+ * Revive a raw column value into a `bigint` for a timestamp / BIGINT column.
320
+ * Accepts an existing `bigint`, an integer `number`, or an all-digits string;
321
+ * passes `null`/`undefined` through (a nullable window aggregate). Anything else
322
+ * — a fractional number, an unexpected type — fails loud rather than silently
323
+ * narrowing precision.
324
+ */
325
+ function reviveBigInt(value: unknown): unknown {
326
+ if (typeof value === "bigint") return value;
327
+ if (typeof value === "number") {
328
+ if (!Number.isInteger(value)) {
329
+ throw new Error(
330
+ `[E_EON_HYDRATE_BIGINT] expected an integer for a bigint/timestamp column, got the non-integer ${value}`,
331
+ );
332
+ }
333
+ if (!Number.isSafeInteger(value)) {
334
+ throw new Error(
335
+ `[E_EON_HYDRATE_BIGINT] integer ${value} exceeds the JS safe-integer range (2^53) and has already lost precision; the transport must hand a bigint/timestamp column back as bigint or a string, not a lossy number`,
336
+ );
337
+ }
338
+ return BigInt(value);
339
+ }
340
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
341
+ return BigInt(value);
342
+ }
343
+ if (value === null || value === undefined) return value;
344
+ throw new Error(
345
+ `[E_EON_HYDRATE_BIGINT] cannot revive a bigint/timestamp column from a ${typeof value}`,
346
+ );
347
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * TypeScript spec shapes mirroring the Rust `crates/eon-query/src/ddl.rs`
3
+ * structs, plus the logical → compiler-kind type map (parallel to atlas
4
+ * `schema/types.ts`).
5
+ *
6
+ * The facade assembles these plain JSON objects from decorator metadata and
7
+ * hands them to `compileStatementNative`; the Rust compiler owns all quoting,
8
+ * type-mapping, and schema-rule validation — the TS side never string-builds
9
+ * SQL (D1, the injection seam stays in Rust).
10
+ */
11
+
12
+ /** The user-facing logical type strings accepted on `@Column`/`@Tag`/`@Timestamp`. */
13
+ export type EonLogicalType =
14
+ | "timestamp"
15
+ | "int"
16
+ | "integer"
17
+ | "bigint"
18
+ | "biginteger"
19
+ | "smallint"
20
+ | "tinyint"
21
+ | "float"
22
+ | "double"
23
+ | "bool"
24
+ | "boolean"
25
+ | "string"
26
+ | "varchar"
27
+ | "nchar"
28
+ | "binary"
29
+ | "varbinary"
30
+ | "json"
31
+ | "decimal";
32
+
33
+ /**
34
+ * Map a logical type string to the compiler's `ColumnTypeKind` (serde
35
+ * camelCase). Common aliases collapse onto one kind (`int`/`integer` → `int`,
36
+ * `string`/`varchar` → `varchar`, `binary`/`varbinary` → `varbinary`). Physical
37
+ * TDengine types are resolved from these kinds in Rust (`Dialect::map_column_type`).
38
+ */
39
+ const TYPE_KIND_ENTRIES = {
40
+ timestamp: "timestamp",
41
+ int: "int",
42
+ integer: "int",
43
+ bigint: "bigInt",
44
+ biginteger: "bigInt",
45
+ smallint: "smallInt",
46
+ tinyint: "tinyInt",
47
+ float: "float",
48
+ double: "double",
49
+ bool: "bool",
50
+ boolean: "bool",
51
+ string: "varchar",
52
+ varchar: "varchar",
53
+ nchar: "nchar",
54
+ binary: "varbinary",
55
+ varbinary: "varbinary",
56
+ json: "json",
57
+ decimal: "decimal",
58
+ // `satisfies` makes this exhaustive + exact against EonLogicalType (a missing
59
+ // or stray key is a compile error) while the exported map stays string-keyed
60
+ // so an arbitrary logical string can be looked up without a cast.
61
+ } satisfies Record<EonLogicalType, string>;
62
+
63
+ export const TYPE_KIND_MAP: Record<string, string> = TYPE_KIND_ENTRIES;
64
+
65
+ /** A column or tag definition — the flattened `StableColumnDef` + `ColumnTypeSpec`. */
66
+ export interface EonColumnSpec {
67
+ name: string;
68
+ kind: string;
69
+ length: number | null;
70
+ precision: number | null;
71
+ scale: number | null;
72
+ }
73
+
74
+ export interface CreateStableSpec {
75
+ kind: "createStable";
76
+ name: string;
77
+ columns: EonColumnSpec[];
78
+ tags: EonColumnSpec[];
79
+ ifNotExists: boolean;
80
+ /** Optional STABLE `KEEP` retention (a 3.3.x+ TDengine feature). Validated as a duration in Rust. */
81
+ keep?: string;
82
+ }
83
+
84
+ /** One `ALTER STABLE` change — tagged on `op`, mirroring the Rust `AlterChange`. */
85
+ export type AlterChange =
86
+ | { op: "addColumn"; name: string; type: Omit<EonColumnSpec, "name"> }
87
+ | { op: "dropColumn"; name: string }
88
+ | { op: "modifyColumn"; name: string; type: Omit<EonColumnSpec, "name"> }
89
+ | { op: "addTag"; name: string; type: Omit<EonColumnSpec, "name"> }
90
+ | { op: "dropTag"; name: string }
91
+ | { op: "modifyTag"; name: string; type: Omit<EonColumnSpec, "name"> }
92
+ | { op: "renameTag"; from: string; to: string };
93
+
94
+ export interface AlterStableSpec {
95
+ kind: "alterStable";
96
+ name: string;
97
+ changes: AlterChange[];
98
+ }
99
+
100
+ export interface CreateChildTableSpec {
101
+ kind: "createChildTable";
102
+ name: string;
103
+ using: string;
104
+ tags: unknown[];
105
+ ifNotExists: boolean;
106
+ /** Inline tag values as SQL literals (58.3 `exec`) vs `?` placeholders (STMT, 58.4). */
107
+ literal: boolean;
108
+ /** Optional child-table `TTL` in whole days (`>= 0`, a 3.3.x+ TDengine feature). */
109
+ ttl?: number;
110
+ }
111
+
112
+ export interface DropStableSpec {
113
+ kind: "dropStable";
114
+ name: string;
115
+ ifExists: boolean;
116
+ }
117
+
118
+ /** TDengine timestamp precision — the create-only `PRECISION` database option. */
119
+ export type EonPrecision = "ms" | "us" | "ns";
120
+
121
+ /** TDengine `CACHEMODEL` allowlist. */
122
+ export type EonCacheModel = "none" | "last_row" | "last_value" | "both";
123
+
124
+ /**
125
+ * `CREATE DATABASE` options (retention / storage). Every value is validated in
126
+ * Rust (durations, precision/cachemodel/wal-level allowlists, `KEEP >= 3x
127
+ * DURATION`). Options are emitted only when present, in a fixed order.
128
+ */
129
+ export interface EonDatabaseOptions {
130
+ /** Retention duration (e.g. `"90d"`). Must be `>= 3 x DURATION` when both are set with the same unit. */
131
+ keep?: string;
132
+ /** Per-file time span (e.g. `"10d"`). Create-only — cannot be altered. */
133
+ duration?: string;
134
+ /** Timestamp precision. Create-only — cannot be altered. */
135
+ precision?: EonPrecision;
136
+ /** Write buffer size in MB. */
137
+ buffer?: number;
138
+ /** WAL level (1 or 2). */
139
+ walLevel?: 1 | 2;
140
+ /** Row cache mode. */
141
+ cachemodel?: EonCacheModel;
142
+ }
143
+
144
+ export interface CreateDatabaseSpec extends EonDatabaseOptions {
145
+ kind: "createDatabase";
146
+ name: string;
147
+ ifNotExists: boolean;
148
+ }
149
+
150
+ /**
151
+ * `ALTER DATABASE` — TDengine changes exactly ONE option per statement, so
152
+ * exactly one field must be set. Create-only options (`PRECISION`/`DURATION`)
153
+ * are intentionally absent here.
154
+ */
155
+ export interface EonAlterDatabaseOptions {
156
+ keep?: string;
157
+ buffer?: number;
158
+ walLevel?: 1 | 2;
159
+ cachemodel?: EonCacheModel;
160
+ replica?: number;
161
+ minrows?: number;
162
+ }
163
+
164
+ export interface AlterDatabaseSpec {
165
+ kind: "alterDatabase";
166
+ name: string;
167
+ /** The Rust-side option name (e.g. `"keep"`, `"wal_level"`). */
168
+ option: string;
169
+ value: string | number;
170
+ }
171
+
172
+ /** A basic (non-super) table: first column `TIMESTAMP`, then columns, no `TAGS`. */
173
+ export interface CreateTableSpec {
174
+ kind: "createTable";
175
+ name: string;
176
+ columns: EonColumnSpec[];
177
+ ifNotExists: boolean;
178
+ }
179
+
180
+ export interface DropTableSpec {
181
+ kind: "dropTable";
182
+ name: string;
183
+ ifExists: boolean;
184
+ }