@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,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EonSchema` — the fluent DDL surface a migration's `up()`/`down()` drives.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors atlas `Schema` (`schema/Schema.ts`) in shape: each method compiles its
|
|
5
|
+
* spec through the Rust `compileStatementNative` seam and appends the resulting
|
|
6
|
+
* SQL to an internal buffer the runner flushes. The TS side NEVER string-builds
|
|
7
|
+
* SQL — every identifier, literal, and option value is quoted/validated in Rust
|
|
8
|
+
* (the injection seam stays in one place, memory `feedback_security_first`).
|
|
9
|
+
*
|
|
10
|
+
* The `StableBuilder` is intentionally THINNER than atlas `TableBuilder`:
|
|
11
|
+
* TDengine columns have no `DEFAULT`, no per-column `PRIMARY KEY` choice (the
|
|
12
|
+
* first `TIMESTAMP` is always the key), no `UNIQUE`, no foreign keys, no SQL
|
|
13
|
+
* secondary indexes — so `defaultTo`/`primary`/`unique`/`references`/`increments`
|
|
14
|
+
* have NO analogue and are deliberately absent (D — the reconciliation note).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { compileStatementNative } from "../query/native.js";
|
|
18
|
+
import type {
|
|
19
|
+
AlterChange,
|
|
20
|
+
AlterDatabaseSpec,
|
|
21
|
+
AlterStableSpec,
|
|
22
|
+
CreateStableSpec,
|
|
23
|
+
CreateTableSpec,
|
|
24
|
+
EonAlterDatabaseOptions,
|
|
25
|
+
EonColumnSpec,
|
|
26
|
+
EonDatabaseOptions,
|
|
27
|
+
} from "./CreateStableSpec.js";
|
|
28
|
+
|
|
29
|
+
/** Build one column/tag spec — the ONE place logical type args become an `EonColumnSpec`. */
|
|
30
|
+
function columnSpec(
|
|
31
|
+
name: string,
|
|
32
|
+
kind: string,
|
|
33
|
+
length: number | null = null,
|
|
34
|
+
precision: number | null = null,
|
|
35
|
+
scale: number | null = null,
|
|
36
|
+
): EonColumnSpec {
|
|
37
|
+
return { name, kind, length, precision, scale };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The type portion of an `EonColumnSpec` (everything but the name). */
|
|
41
|
+
type EonType = Omit<EonColumnSpec, "name">;
|
|
42
|
+
|
|
43
|
+
/** Build the type portion — the ONE place logical type args become an `EonType`. */
|
|
44
|
+
function makeType(
|
|
45
|
+
kind: string,
|
|
46
|
+
length: number | null = null,
|
|
47
|
+
precision: number | null = null,
|
|
48
|
+
scale: number | null = null,
|
|
49
|
+
): EonType {
|
|
50
|
+
return { kind, length, precision, scale };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The thin column/tag surface shared by every builder: only column-type methods
|
|
55
|
+
* (+ length/precision/scale). No `defaultTo`/`primary`/`unique` — TDengine has
|
|
56
|
+
* no analogue. Each method appends a column via `#add` and returns whatever that
|
|
57
|
+
* hook returns (a tag-handle for stables, `void` for basic tables).
|
|
58
|
+
*/
|
|
59
|
+
abstract class ColumnMethods<R> {
|
|
60
|
+
protected abstract add(spec: EonColumnSpec): R;
|
|
61
|
+
|
|
62
|
+
timestamp(name: string): R {
|
|
63
|
+
return this.add(columnSpec(name, "timestamp"));
|
|
64
|
+
}
|
|
65
|
+
int(name: string): R {
|
|
66
|
+
return this.add(columnSpec(name, "int"));
|
|
67
|
+
}
|
|
68
|
+
bigInteger(name: string): R {
|
|
69
|
+
return this.add(columnSpec(name, "bigInt"));
|
|
70
|
+
}
|
|
71
|
+
float(name: string): R {
|
|
72
|
+
return this.add(columnSpec(name, "float"));
|
|
73
|
+
}
|
|
74
|
+
double(name: string): R {
|
|
75
|
+
return this.add(columnSpec(name, "double"));
|
|
76
|
+
}
|
|
77
|
+
bool(name: string): R {
|
|
78
|
+
return this.add(columnSpec(name, "bool"));
|
|
79
|
+
}
|
|
80
|
+
varchar(name: string, length: number): R {
|
|
81
|
+
return this.add(columnSpec(name, "varchar", length));
|
|
82
|
+
}
|
|
83
|
+
nchar(name: string, length: number): R {
|
|
84
|
+
return this.add(columnSpec(name, "nchar", length));
|
|
85
|
+
}
|
|
86
|
+
binary(name: string, length: number): R {
|
|
87
|
+
return this.add(columnSpec(name, "varbinary", length));
|
|
88
|
+
}
|
|
89
|
+
decimal(name: string, precision: number, scale?: number): R {
|
|
90
|
+
return this.add(
|
|
91
|
+
columnSpec(name, "decimal", null, precision, scale ?? null),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
json(name: string): R {
|
|
95
|
+
return this.add(columnSpec(name, "json"));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The no-name type surface for an `ALTER STABLE` add/modify change: the column
|
|
101
|
+
* name is already fixed (by `addColumn(name)` etc.), so these methods take ONLY
|
|
102
|
+
* the type arguments and finalize the change.
|
|
103
|
+
*/
|
|
104
|
+
abstract class TypeMethods<R> {
|
|
105
|
+
protected abstract emit(type: EonType): R;
|
|
106
|
+
|
|
107
|
+
timestamp(): R {
|
|
108
|
+
return this.emit(makeType("timestamp"));
|
|
109
|
+
}
|
|
110
|
+
int(): R {
|
|
111
|
+
return this.emit(makeType("int"));
|
|
112
|
+
}
|
|
113
|
+
bigInteger(): R {
|
|
114
|
+
return this.emit(makeType("bigInt"));
|
|
115
|
+
}
|
|
116
|
+
float(): R {
|
|
117
|
+
return this.emit(makeType("float"));
|
|
118
|
+
}
|
|
119
|
+
double(): R {
|
|
120
|
+
return this.emit(makeType("double"));
|
|
121
|
+
}
|
|
122
|
+
bool(): R {
|
|
123
|
+
return this.emit(makeType("bool"));
|
|
124
|
+
}
|
|
125
|
+
varchar(length: number): R {
|
|
126
|
+
return this.emit(makeType("varchar", length));
|
|
127
|
+
}
|
|
128
|
+
nchar(length: number): R {
|
|
129
|
+
return this.emit(makeType("nchar", length));
|
|
130
|
+
}
|
|
131
|
+
binary(length: number): R {
|
|
132
|
+
return this.emit(makeType("varbinary", length));
|
|
133
|
+
}
|
|
134
|
+
decimal(precision: number, scale?: number): R {
|
|
135
|
+
return this.emit(makeType("decimal", null, precision, scale ?? null));
|
|
136
|
+
}
|
|
137
|
+
json(): R {
|
|
138
|
+
return this.emit(makeType("json"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A handle returned by a `StableBuilder` column method — `.tag()` re-homes it as a tag. */
|
|
143
|
+
export interface StableColumnHandle {
|
|
144
|
+
/** Mark the just-declared column as a `TAG` instead of a metric column. */
|
|
145
|
+
tag(): void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Fluent builder for `CREATE STABLE` columns + tags (ts-first is enforced in Rust). */
|
|
149
|
+
export class StableBuilder extends ColumnMethods<StableColumnHandle> {
|
|
150
|
+
readonly #columns: EonColumnSpec[] = [];
|
|
151
|
+
readonly #tags: EonColumnSpec[] = [];
|
|
152
|
+
|
|
153
|
+
protected add(spec: EonColumnSpec): StableColumnHandle {
|
|
154
|
+
this.#columns.push(spec);
|
|
155
|
+
return {
|
|
156
|
+
tag: () => {
|
|
157
|
+
// Idempotent: a second `.tag()` on the same handle finds the spec
|
|
158
|
+
// already re-homed (i < 0) and must NOT push a duplicate tag.
|
|
159
|
+
const i = this.#columns.indexOf(spec);
|
|
160
|
+
if (i >= 0) {
|
|
161
|
+
this.#columns.splice(i, 1);
|
|
162
|
+
this.#tags.push(spec);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @internal */
|
|
169
|
+
columns(): EonColumnSpec[] {
|
|
170
|
+
return this.#columns;
|
|
171
|
+
}
|
|
172
|
+
/** @internal */
|
|
173
|
+
tags(): EonColumnSpec[] {
|
|
174
|
+
return this.#tags;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Fluent builder for a basic (non-super) table — same columns, no tags. */
|
|
179
|
+
export class BasicTableBuilder extends ColumnMethods<void> {
|
|
180
|
+
readonly #columns: EonColumnSpec[] = [];
|
|
181
|
+
|
|
182
|
+
protected add(spec: EonColumnSpec): void {
|
|
183
|
+
this.#columns.push(spec);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** @internal */
|
|
187
|
+
columns(): EonColumnSpec[] {
|
|
188
|
+
return this.#columns;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Sets the type on a pending `ALTER STABLE` add/modify change, then records it. */
|
|
193
|
+
export interface AlterTypeSetter extends TypeMethods<void> {}
|
|
194
|
+
class AlterTypeSetterImpl extends TypeMethods<void> {
|
|
195
|
+
readonly #record: (type: EonType) => void;
|
|
196
|
+
constructor(record: (type: EonType) => void) {
|
|
197
|
+
super();
|
|
198
|
+
this.#record = record;
|
|
199
|
+
}
|
|
200
|
+
protected emit(type: EonType): void {
|
|
201
|
+
this.#record(type);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Fluent builder for `ALTER STABLE` — one method per TDengine change op. */
|
|
206
|
+
export class AlterStableBuilder {
|
|
207
|
+
readonly #changes: AlterChange[] = [];
|
|
208
|
+
|
|
209
|
+
addColumn(name: string): AlterTypeSetter {
|
|
210
|
+
return new AlterTypeSetterImpl((type) =>
|
|
211
|
+
this.#changes.push({ op: "addColumn", name, type }),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
modifyColumn(name: string): AlterTypeSetter {
|
|
215
|
+
return new AlterTypeSetterImpl((type) =>
|
|
216
|
+
this.#changes.push({ op: "modifyColumn", name, type }),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
dropColumn(name: string): this {
|
|
220
|
+
this.#changes.push({ op: "dropColumn", name });
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
addTag(name: string): AlterTypeSetter {
|
|
224
|
+
return new AlterTypeSetterImpl((type) =>
|
|
225
|
+
this.#changes.push({ op: "addTag", name, type }),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
modifyTag(name: string): AlterTypeSetter {
|
|
229
|
+
return new AlterTypeSetterImpl((type) =>
|
|
230
|
+
this.#changes.push({ op: "modifyTag", name, type }),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
dropTag(name: string): this {
|
|
234
|
+
this.#changes.push({ op: "dropTag", name });
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
renameTag(from: string, to: string): this {
|
|
238
|
+
this.#changes.push({ op: "renameTag", from, to });
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @internal */
|
|
243
|
+
changes(): AlterChange[] {
|
|
244
|
+
return this.#changes;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Map an `alterDatabase` option key to its Rust-side option name. */
|
|
249
|
+
const ALTER_DB_OPTION_NAMES: Record<keyof EonAlterDatabaseOptions, string> = {
|
|
250
|
+
keep: "keep",
|
|
251
|
+
buffer: "buffer",
|
|
252
|
+
walLevel: "wal_level",
|
|
253
|
+
cachemodel: "cachemodel",
|
|
254
|
+
replica: "replica",
|
|
255
|
+
minrows: "minrows",
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export class EonSchema {
|
|
259
|
+
#statements: string[] = [];
|
|
260
|
+
|
|
261
|
+
#push(spec: object): void {
|
|
262
|
+
const { statements } = compileStatementNative(spec, "tdengine");
|
|
263
|
+
this.#statements.push(...statements);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* `CREATE STABLE`. Defaults to `IF NOT EXISTS` (idempotent DDL — the mitigation
|
|
268
|
+
* for TDengine's non-transactional migrations, AC6). Pass `keep` for retention.
|
|
269
|
+
*/
|
|
270
|
+
createStable(
|
|
271
|
+
name: string,
|
|
272
|
+
callback: (table: StableBuilder) => void,
|
|
273
|
+
options: { ifNotExists?: boolean; keep?: string } = {},
|
|
274
|
+
): this {
|
|
275
|
+
const builder = new StableBuilder();
|
|
276
|
+
callback(builder);
|
|
277
|
+
const spec: CreateStableSpec = {
|
|
278
|
+
kind: "createStable",
|
|
279
|
+
name,
|
|
280
|
+
columns: builder.columns(),
|
|
281
|
+
tags: builder.tags(),
|
|
282
|
+
ifNotExists: options.ifNotExists ?? true,
|
|
283
|
+
...(options.keep !== undefined ? { keep: options.keep } : {}),
|
|
284
|
+
};
|
|
285
|
+
this.#push(spec);
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** `ALTER STABLE` → one statement per change. */
|
|
290
|
+
alterStable(
|
|
291
|
+
name: string,
|
|
292
|
+
callback: (table: AlterStableBuilder) => void,
|
|
293
|
+
): this {
|
|
294
|
+
const builder = new AlterStableBuilder();
|
|
295
|
+
callback(builder);
|
|
296
|
+
const spec: AlterStableSpec = {
|
|
297
|
+
kind: "alterStable",
|
|
298
|
+
name,
|
|
299
|
+
changes: builder.changes(),
|
|
300
|
+
};
|
|
301
|
+
this.#push(spec);
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** `DROP STABLE IF EXISTS`. */
|
|
306
|
+
dropStable(name: string): this {
|
|
307
|
+
this.#push({ kind: "dropStable", name, ifExists: true });
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** `CREATE DATABASE IF NOT EXISTS` with optional retention/storage options. */
|
|
312
|
+
createDatabase(name: string, options: EonDatabaseOptions = {}): this {
|
|
313
|
+
this.#push({
|
|
314
|
+
kind: "createDatabase",
|
|
315
|
+
name,
|
|
316
|
+
ifNotExists: true,
|
|
317
|
+
...options,
|
|
318
|
+
});
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `ALTER DATABASE` — exactly ONE option per call (TDengine restriction).
|
|
324
|
+
* Throws if zero or more than one option key is provided.
|
|
325
|
+
*/
|
|
326
|
+
alterDatabase(name: string, options: EonAlterDatabaseOptions): this {
|
|
327
|
+
const keys: (keyof EonAlterDatabaseOptions)[] = [
|
|
328
|
+
"keep",
|
|
329
|
+
"buffer",
|
|
330
|
+
"walLevel",
|
|
331
|
+
"cachemodel",
|
|
332
|
+
"replica",
|
|
333
|
+
"minrows",
|
|
334
|
+
];
|
|
335
|
+
const set = keys.filter((k) => options[k] !== undefined);
|
|
336
|
+
const key = set[0];
|
|
337
|
+
if (set.length !== 1 || key === undefined) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`[E_EON_ALTER_DB_ONE_OPTION] alterDatabase('${name}', …) requires exactly one option, got ${set.length}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const value = options[key];
|
|
343
|
+
if (value === undefined) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`[E_EON_ALTER_DB_ONE_OPTION] alterDatabase('${name}', …) option '${key}' has no value`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const spec: AlterDatabaseSpec = {
|
|
349
|
+
kind: "alterDatabase",
|
|
350
|
+
name,
|
|
351
|
+
option: ALTER_DB_OPTION_NAMES[key],
|
|
352
|
+
value,
|
|
353
|
+
};
|
|
354
|
+
this.#push(spec);
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** `CREATE TABLE IF NOT EXISTS` — a basic (non-super) table, no tags. */
|
|
359
|
+
createTable(
|
|
360
|
+
name: string,
|
|
361
|
+
callback: (table: BasicTableBuilder) => void,
|
|
362
|
+
): this {
|
|
363
|
+
const builder = new BasicTableBuilder();
|
|
364
|
+
callback(builder);
|
|
365
|
+
const spec: CreateTableSpec = {
|
|
366
|
+
kind: "createTable",
|
|
367
|
+
name,
|
|
368
|
+
columns: builder.columns(),
|
|
369
|
+
ifNotExists: true,
|
|
370
|
+
};
|
|
371
|
+
this.#push(spec);
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** `DROP TABLE IF EXISTS`. */
|
|
376
|
+
dropTable(name: string): this {
|
|
377
|
+
this.#push({ kind: "dropTable", name, ifExists: true });
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Queue a raw SQL statement verbatim (an escape hatch for options not modelled above). */
|
|
382
|
+
raw(sql: string): this {
|
|
383
|
+
this.#statements.push(sql);
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Clear the buffer (called before each `up()`/`down()` run). */
|
|
388
|
+
reset(): void {
|
|
389
|
+
this.#statements = [];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The compiled SQL statements queued so far. */
|
|
393
|
+
toSQL(): string[] {
|
|
394
|
+
return [...this.#statements];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Migration` — extend this to author a versioned eon (TDengine) migration.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors atlas `Migration` (`schema/Migration.ts`): `up()`/`down()` drive a
|
|
5
|
+
* fluent `EonSchema`, and `getUpSQL()`/`getDownSQL()` replay the callback to
|
|
6
|
+
* collect the compiled SQL. Single-dialect (TDengine) so — unlike atlas — the
|
|
7
|
+
* constructor takes NO dialect argument, and there is no `now()`/`defaultTo`
|
|
8
|
+
* helper (TDengine columns have no `DEFAULT`).
|
|
9
|
+
*
|
|
10
|
+
* TDengine caveat (AC6): DDL is non-transactional and some operations are
|
|
11
|
+
* irreversible (a `MODIFY` length-extension cannot be un-extended, a dropped
|
|
12
|
+
* column's data is gone) — `down()` is best-effort. Prefer idempotent DDL
|
|
13
|
+
* (`IF (NOT) EXISTS`, the `EonSchema` default) so a partially-applied migration
|
|
14
|
+
* re-runs cleanly.
|
|
15
|
+
*
|
|
16
|
+
* import { Migration } from "@c9up/eon";
|
|
17
|
+
*
|
|
18
|
+
* export default class CreateMeters extends Migration {
|
|
19
|
+
* up() {
|
|
20
|
+
* this.schema.createDatabase("metrics", { keep: "90d", duration: "10d" });
|
|
21
|
+
* this.schema.createStable("meters", (t) => {
|
|
22
|
+
* t.timestamp("ts");
|
|
23
|
+
* t.float("current");
|
|
24
|
+
* t.int("groupid").tag();
|
|
25
|
+
* });
|
|
26
|
+
* }
|
|
27
|
+
* down() {
|
|
28
|
+
* this.schema.dropStable("meters");
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { EonSchema } from "./EonSchema.js";
|
|
34
|
+
|
|
35
|
+
export abstract class Migration {
|
|
36
|
+
readonly schema: EonSchema;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.schema = new EonSchema();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Apply the migration. */
|
|
43
|
+
abstract up(): Promise<void> | void;
|
|
44
|
+
|
|
45
|
+
/** Reverse the migration (best-effort on TDengine — see class docs). */
|
|
46
|
+
abstract down(): Promise<void> | void;
|
|
47
|
+
|
|
48
|
+
/** Queue a raw SQL statement verbatim (escape hatch for un-modelled DDL). */
|
|
49
|
+
raw(sql: string): void {
|
|
50
|
+
this.schema.raw(sql);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compile the SQL statements this migration's `up()` produces. */
|
|
54
|
+
async getUpSQL(): Promise<string[]> {
|
|
55
|
+
this.schema.reset();
|
|
56
|
+
await this.up();
|
|
57
|
+
return this.schema.toSQL();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Compile the SQL statements this migration's `down()` produces. */
|
|
61
|
+
async getDownSQL(): Promise<string[]> {
|
|
62
|
+
this.schema.reset();
|
|
63
|
+
await this.down();
|
|
64
|
+
return this.schema.toSQL();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble a `CreateStableSpec` from a decorated super-table class.
|
|
3
|
+
*
|
|
4
|
+
* Reads schema through the metadata getters ONLY (never `key in instance` — the
|
|
5
|
+
* `declare` pitfall, memory `project_atlas_declare_hydration`), places the
|
|
6
|
+
* timestamp column first (TDengine's PK rule), and normalises each logical type
|
|
7
|
+
* string to the compiler kind via `TYPE_KIND_MAP`. Mirror of atlas
|
|
8
|
+
* `TableBuilder.toStatements` spec-build.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type EonColumnMetadata,
|
|
13
|
+
type EonTagMetadata,
|
|
14
|
+
getColumnMetadata,
|
|
15
|
+
getSuperTableMetadata,
|
|
16
|
+
getTagMetadata,
|
|
17
|
+
getTimestampColumn,
|
|
18
|
+
} from "../decorators/superTable.js";
|
|
19
|
+
import {
|
|
20
|
+
type CreateStableSpec,
|
|
21
|
+
type EonColumnSpec,
|
|
22
|
+
TYPE_KIND_MAP,
|
|
23
|
+
} from "./CreateStableSpec.js";
|
|
24
|
+
|
|
25
|
+
/** A decorated super-table class. Every constructor carries `.name` (`Function.name`). */
|
|
26
|
+
export type SuperTableClass = (abstract new (
|
|
27
|
+
...args: never[]
|
|
28
|
+
) => object) & {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Resolve the super-table name for a class, or throw if it is undecorated. */
|
|
33
|
+
export function requireSuperTableName(EntityClass: SuperTableClass): string {
|
|
34
|
+
const meta = getSuperTableMetadata(EntityClass);
|
|
35
|
+
if (!meta) {
|
|
36
|
+
const label =
|
|
37
|
+
EntityClass.name.length > 0 ? EntityClass.name : "the given class";
|
|
38
|
+
throw new Error(
|
|
39
|
+
`[E_EON_NOT_A_SUPERTABLE] ${label} is not decorated with @SuperTable`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return meta.name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The stable's physical column order: timestamp first (TDengine's PK rule), then
|
|
47
|
+
* declaration order. The STMT columnar bind is POSITIONAL (the connector's
|
|
48
|
+
* prepare template omits the value-column list), so its bind order MUST match the
|
|
49
|
+
* order the STABLE was created with. Both `compileCreateStableSpec` (the DDL) and
|
|
50
|
+
* `SuperTableRepository`'s ingest plan derive their order from THIS one function
|
|
51
|
+
* so the two can never silently drift apart and write values into wrong columns.
|
|
52
|
+
*/
|
|
53
|
+
export function orderTimestampFirst<T extends { propertyKey: string }>(
|
|
54
|
+
columns: readonly T[],
|
|
55
|
+
tsColumn: T,
|
|
56
|
+
tsProperty: string,
|
|
57
|
+
): T[] {
|
|
58
|
+
return [tsColumn, ...columns.filter((c) => c.propertyKey !== tsProperty)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toColumnSpec(meta: EonColumnMetadata | EonTagMetadata): EonColumnSpec {
|
|
62
|
+
const logical = (meta.type ?? "").toLowerCase();
|
|
63
|
+
const kind = TYPE_KIND_MAP[logical];
|
|
64
|
+
if (!kind) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[E_EON_TYPE] column/tag '${meta.propertyKey}' has an unknown or missing type '${meta.type ?? ""}'`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
name: meta.propertyKey,
|
|
71
|
+
kind,
|
|
72
|
+
length: meta.length ?? null,
|
|
73
|
+
precision: meta.precision ?? null,
|
|
74
|
+
scale: meta.scale ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the `createStable` spec (ts column first). `ifNotExists` defaults to
|
|
80
|
+
* `false`; `syncSuperTable` overrides it to `true`.
|
|
81
|
+
*/
|
|
82
|
+
export function compileCreateStableSpec(
|
|
83
|
+
EntityClass: SuperTableClass,
|
|
84
|
+
): CreateStableSpec {
|
|
85
|
+
const name = requireSuperTableName(EntityClass);
|
|
86
|
+
const tsProperty = getTimestampColumn(EntityClass);
|
|
87
|
+
if (!tsProperty) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[E_EON_NO_TIMESTAMP] super-table '${name}' has no @Timestamp column`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const columns = getColumnMetadata(EntityClass);
|
|
94
|
+
const tsColumn = columns.find((c) => c.propertyKey === tsProperty);
|
|
95
|
+
if (!tsColumn) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`[E_EON_NO_TIMESTAMP] super-table '${name}' @Timestamp column '${tsProperty}' is not registered`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const orderedColumns = orderTimestampFirst(columns, tsColumn, tsProperty);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
kind: "createStable",
|
|
104
|
+
name,
|
|
105
|
+
columns: orderedColumns.map(toColumnSpec),
|
|
106
|
+
tags: getTagMetadata(EntityClass).map(toColumnSpec),
|
|
107
|
+
ifNotExists: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema facade — drive TDengine DDL from decorator metadata through a live
|
|
3
|
+
* `EonConnection`. Agnostic leaf: takes the connection structurally, never
|
|
4
|
+
* imports `@c9up/ream`.
|
|
5
|
+
*
|
|
6
|
+
* Every statement is compiled in Rust (`compileStatementNative`) and run through
|
|
7
|
+
* the literal-only `EonConnection.exec` (58.2 D3). Child-table creation renders
|
|
8
|
+
* its tag values as inlined SQL literals (`literal: true`, D4) so `exec` can run
|
|
9
|
+
* it directly; the `?`-bound STMT form lands with ingest in 58.4.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { EonConnection } from "../connection/EonConnection.js";
|
|
13
|
+
import { compileStatementNative } from "../query/native.js";
|
|
14
|
+
import type {
|
|
15
|
+
CreateChildTableSpec,
|
|
16
|
+
DropStableSpec,
|
|
17
|
+
} from "./CreateStableSpec.js";
|
|
18
|
+
import {
|
|
19
|
+
compileCreateStableSpec,
|
|
20
|
+
requireSuperTableName,
|
|
21
|
+
type SuperTableClass,
|
|
22
|
+
} from "./compile.js";
|
|
23
|
+
|
|
24
|
+
/** Compile a spec and run each resulting statement through the connection. */
|
|
25
|
+
async function execSpec(conn: EonConnection, spec: object): Promise<void> {
|
|
26
|
+
const { statements } = compileStatementNative(spec, "tdengine");
|
|
27
|
+
for (const sql of statements) {
|
|
28
|
+
await conn.exec(sql);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Create the super-table for a decorated class (`CREATE STABLE IF NOT EXISTS`). */
|
|
33
|
+
export function syncSuperTable(
|
|
34
|
+
conn: EonConnection,
|
|
35
|
+
EntityClass: SuperTableClass,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const spec = { ...compileCreateStableSpec(EntityClass), ifNotExists: true };
|
|
38
|
+
return execSpec(conn, spec);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Options for {@link createChildTable}: identify the stable by name or class. */
|
|
42
|
+
export interface CreateChildTableOptions {
|
|
43
|
+
stable?: string;
|
|
44
|
+
EntityClass?: SuperTableClass;
|
|
45
|
+
name: string;
|
|
46
|
+
tags: unknown[];
|
|
47
|
+
ifNotExists?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create an explicit child table bound to its super-table's tag values
|
|
52
|
+
* (`CREATE TABLE name USING stable TAGS (...)`). Tag values are inlined as typed
|
|
53
|
+
* SQL literals (D4).
|
|
54
|
+
*/
|
|
55
|
+
export function createChildTable(
|
|
56
|
+
conn: EonConnection,
|
|
57
|
+
options: CreateChildTableOptions,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const using = resolveStableName(options);
|
|
60
|
+
const spec: CreateChildTableSpec = {
|
|
61
|
+
kind: "createChildTable",
|
|
62
|
+
name: options.name,
|
|
63
|
+
using,
|
|
64
|
+
tags: options.tags,
|
|
65
|
+
ifNotExists: options.ifNotExists ?? true,
|
|
66
|
+
literal: true,
|
|
67
|
+
};
|
|
68
|
+
return execSpec(conn, spec);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Drop the super-table for a decorated class (`DROP STABLE IF EXISTS`). */
|
|
72
|
+
export function dropSuperTable(
|
|
73
|
+
conn: EonConnection,
|
|
74
|
+
EntityClass: SuperTableClass,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const spec: DropStableSpec = {
|
|
77
|
+
kind: "dropStable",
|
|
78
|
+
name: requireSuperTableName(EntityClass),
|
|
79
|
+
ifExists: true,
|
|
80
|
+
};
|
|
81
|
+
return execSpec(conn, spec);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A deterministic child-table name for a stable + tag-set, so the same tags
|
|
86
|
+
* always map to the same child (idempotent create, stable routing for 58.4
|
|
87
|
+
* ingest). Dependency-free FNV-1a — no `crypto`, no npm dep (D6). Pass an
|
|
88
|
+
* explicit `name` to {@link createChildTable} to override.
|
|
89
|
+
*/
|
|
90
|
+
export function childTableName(
|
|
91
|
+
stableOrEntity: string | SuperTableClass,
|
|
92
|
+
tagValues: unknown[],
|
|
93
|
+
): string {
|
|
94
|
+
const stable =
|
|
95
|
+
typeof stableOrEntity === "string"
|
|
96
|
+
? stableOrEntity
|
|
97
|
+
: requireSuperTableName(stableOrEntity);
|
|
98
|
+
const key = [stable, ...tagValues.map(stringifyTagValue)].join("\u0000");
|
|
99
|
+
return `t_${fnv1a(key)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveStableName(options: CreateChildTableOptions): string {
|
|
103
|
+
if (typeof options.stable === "string" && options.stable.length > 0) {
|
|
104
|
+
return options.stable;
|
|
105
|
+
}
|
|
106
|
+
if (options.EntityClass) {
|
|
107
|
+
return requireSuperTableName(options.EntityClass);
|
|
108
|
+
}
|
|
109
|
+
throw new Error(
|
|
110
|
+
"[E_EON_CHILD_STABLE] createChildTable requires either `stable` (name) or `EntityClass`",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A tag value's stable string form for the child-name key. `JSON.stringify`
|
|
116
|
+
* throws on a `bigint` (`TypeError: Do not know how to serialize a BigInt`),
|
|
117
|
+
* which would surface as an opaque crash for any BIGINT `@Tag`; render bigints
|
|
118
|
+
* explicitly so every tag type maps to a deterministic, typed-out string.
|
|
119
|
+
*/
|
|
120
|
+
export function stringifyTagValue(value: unknown): string {
|
|
121
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
122
|
+
return JSON.stringify(value) ?? "null";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 64-bit FNV-1a over UTF-16 code units → zero-padded 16-char hex. 64 bits keeps
|
|
127
|
+
* the child-name collision probability negligible even at millions of distinct
|
|
128
|
+
* tag-sets; a 32-bit space collides ~50% by only ~77k children (realistic
|
|
129
|
+
* per-device TSDB cardinality), which would silently write one device's rows
|
|
130
|
+
* under another's tags. `groupByChild` additionally asserts no two distinct
|
|
131
|
+
* tag-sets ever share a name.
|
|
132
|
+
*/
|
|
133
|
+
function fnv1a(input: string): string {
|
|
134
|
+
const MASK = (1n << 64n) - 1n;
|
|
135
|
+
const PRIME = 0x00000100000001b3n;
|
|
136
|
+
let hash = 0xcbf29ce484222325n;
|
|
137
|
+
for (let i = 0; i < input.length; i++) {
|
|
138
|
+
hash = (hash ^ BigInt(input.charCodeAt(i))) & MASK;
|
|
139
|
+
hash = (hash * PRIME) & MASK;
|
|
140
|
+
}
|
|
141
|
+
return hash.toString(16).padStart(16, "0");
|
|
142
|
+
}
|