@stonyx/orm 0.2.1-beta.91 → 0.2.1-beta.92
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/dist/postgres/migration-generator.js +19 -0
- package/dist/postgres/schema-introspector.d.ts +2 -1
- package/dist/postgres/schema-introspector.js +18 -2
- package/dist/types/orm-types.d.ts +11 -0
- package/package.json +1 -1
- package/src/postgres/migration-generator.ts +22 -0
- package/src/postgres/schema-introspector.ts +20 -3
- package/src/types/orm-types.ts +12 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { introspectModels, introspectViews, buildTableDDL, buildViewDDL, buildVectorIndexDDL, schemasToSnapshot, viewSchemasToSnapshot, getTopologicalOrder } from './schema-introspector.js';
|
|
2
|
+
import { buildCreateHypertable, buildEnableCompression, buildCompressionPolicy } from '../timescale/query-builder.js';
|
|
2
3
|
import { readFile, createFile, createDirectory, fileExists } from '@stonyx/utils/file';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import config from 'stonyx/config';
|
|
@@ -41,6 +42,24 @@ export async function generateMigration(description = 'migration', configKey = '
|
|
|
41
42
|
}
|
|
42
43
|
downStatements.unshift(`DROP TABLE IF EXISTS "${schemas[name].table}" CASCADE;`);
|
|
43
44
|
}
|
|
45
|
+
// Hypertable conversion + compression (TimescaleDB only)
|
|
46
|
+
if (configKey === 'timescale') {
|
|
47
|
+
for (const name of addedOrdered) {
|
|
48
|
+
const schema = schemas[name];
|
|
49
|
+
if (!schema.hypertable)
|
|
50
|
+
continue;
|
|
51
|
+
const { timeColumn, chunkInterval } = schema.hypertable;
|
|
52
|
+
upStatements.push(buildCreateHypertable(schema.table, timeColumn, { chunkInterval }).sql + ';');
|
|
53
|
+
if (schema.hypertable.compress) {
|
|
54
|
+
const { segmentBy, orderBy, after } = schema.hypertable.compress;
|
|
55
|
+
upStatements.push(buildEnableCompression(schema.table, segmentBy, orderBy).sql + ';');
|
|
56
|
+
if (after) {
|
|
57
|
+
upStatements.push(buildCompressionPolicy(schema.table, after).sql + ';');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
downStatements.unshift('-- Hypertable conversion is not reversible; table drop handles cleanup');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
44
63
|
// Removed tables (warn only, commented out)
|
|
45
64
|
for (const name of diff.removedModels) {
|
|
46
65
|
upStatements.push(`-- WARNING: Model '${name}' was removed. Uncomment to drop table:`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ForeignKeyDef, ModelSchema, ViewSchema } from '../types/orm-types.js';
|
|
1
|
+
import type { ForeignKeyDef, HypertableConfig, ModelSchema, ViewSchema } from '../types/orm-types.js';
|
|
2
2
|
interface ViewSnapshotEntry {
|
|
3
3
|
viewName: string;
|
|
4
4
|
source: string;
|
|
@@ -14,6 +14,7 @@ interface ModelSnapshotEntry {
|
|
|
14
14
|
columns: Record<string, string>;
|
|
15
15
|
foreignKeys: Record<string, ForeignKeyDef>;
|
|
16
16
|
vectorColumns?: Record<string, number>;
|
|
17
|
+
hypertable?: HypertableConfig;
|
|
17
18
|
}
|
|
18
19
|
export declare function introspectModels(): Record<string, ModelSchema>;
|
|
19
20
|
export declare function buildTableDDL(name: string, schema: ModelSchema, allSchemas?: Record<string, ModelSchema>): string;
|
|
@@ -55,6 +55,7 @@ export function introspectModels() {
|
|
|
55
55
|
column: 'id',
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
+
const hypertable = modelClass.hypertable;
|
|
58
59
|
schemas[name] = {
|
|
59
60
|
table: sanitizeTableName(getPluralName(name)),
|
|
60
61
|
idType,
|
|
@@ -62,19 +63,24 @@ export function introspectModels() {
|
|
|
62
63
|
foreignKeys,
|
|
63
64
|
relationships,
|
|
64
65
|
vectorColumns,
|
|
66
|
+
hypertable: hypertable || undefined,
|
|
65
67
|
memory: modelClass.memory === true,
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
return schemas;
|
|
69
71
|
}
|
|
70
72
|
export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
71
|
-
const { idType, columns, foreignKeys } = schema;
|
|
73
|
+
const { idType, columns, foreignKeys, hypertable } = schema;
|
|
72
74
|
const table = sanitizeTableName(schema.table);
|
|
73
75
|
const lines = [];
|
|
76
|
+
const useCompositePK = hypertable && idType !== 'string';
|
|
74
77
|
// Primary key
|
|
75
78
|
if (idType === 'string') {
|
|
76
79
|
lines.push(' "id" VARCHAR(255) PRIMARY KEY');
|
|
77
80
|
}
|
|
81
|
+
else if (useCompositePK) {
|
|
82
|
+
lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY');
|
|
83
|
+
}
|
|
78
84
|
else {
|
|
79
85
|
lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY');
|
|
80
86
|
}
|
|
@@ -88,13 +94,22 @@ export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
|
88
94
|
lines.push(` "${fkCol}" ${refIdType}`);
|
|
89
95
|
}
|
|
90
96
|
// Timestamps
|
|
91
|
-
|
|
97
|
+
if (useCompositePK) {
|
|
98
|
+
lines.push(' "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
|
|
102
|
+
}
|
|
92
103
|
lines.push(' "updated_at" TIMESTAMPTZ DEFAULT NOW()');
|
|
93
104
|
// Foreign key constraints
|
|
94
105
|
for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
|
|
95
106
|
const refTable = sanitizeTableName(fkDef.references);
|
|
96
107
|
lines.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${refTable}"("${fkDef.column}") ON DELETE SET NULL`);
|
|
97
108
|
}
|
|
109
|
+
// Composite primary key for hypertable models
|
|
110
|
+
if (useCompositePK) {
|
|
111
|
+
lines.push(` PRIMARY KEY ("id", "${hypertable.timeColumn}")`);
|
|
112
|
+
}
|
|
98
113
|
return `CREATE TABLE IF NOT EXISTS "${table}" (\n${lines.join(',\n')}\n)`;
|
|
99
114
|
}
|
|
100
115
|
/**
|
|
@@ -274,6 +289,7 @@ export function schemasToSnapshot(schemas) {
|
|
|
274
289
|
...(schema.vectorColumns && Object.keys(schema.vectorColumns).length > 0
|
|
275
290
|
? { vectorColumns: { ...schema.vectorColumns } }
|
|
276
291
|
: {}),
|
|
292
|
+
...(schema.hypertable ? { hypertable: schema.hypertable } : {}),
|
|
277
293
|
};
|
|
278
294
|
}
|
|
279
295
|
return snapshot;
|
|
@@ -86,6 +86,15 @@ export interface ForeignKeyDef {
|
|
|
86
86
|
references: string;
|
|
87
87
|
column: string;
|
|
88
88
|
}
|
|
89
|
+
export interface HypertableConfig {
|
|
90
|
+
timeColumn: string;
|
|
91
|
+
chunkInterval?: string;
|
|
92
|
+
compress?: {
|
|
93
|
+
segmentBy?: string;
|
|
94
|
+
orderBy?: string;
|
|
95
|
+
after?: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
89
98
|
export interface ModelSchema {
|
|
90
99
|
table: string;
|
|
91
100
|
idType: string;
|
|
@@ -96,6 +105,7 @@ export interface ModelSchema {
|
|
|
96
105
|
hasMany: Record<string, string | null>;
|
|
97
106
|
};
|
|
98
107
|
vectorColumns?: Record<string, number>;
|
|
108
|
+
hypertable?: HypertableConfig;
|
|
99
109
|
memory: boolean;
|
|
100
110
|
}
|
|
101
111
|
export interface ViewSchema {
|
|
@@ -135,6 +145,7 @@ export interface SnapshotEntry {
|
|
|
135
145
|
columns?: Record<string, string>;
|
|
136
146
|
foreignKeys?: Record<string, ForeignKeyDef>;
|
|
137
147
|
vectorColumns?: Record<string, number>;
|
|
148
|
+
hypertable?: HypertableConfig;
|
|
138
149
|
isView?: boolean;
|
|
139
150
|
viewName?: string;
|
|
140
151
|
source?: string;
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { introspectModels, introspectViews, buildTableDDL, buildViewDDL, buildVectorIndexDDL, schemasToSnapshot, viewSchemasToSnapshot, getTopologicalOrder } from './schema-introspector.js';
|
|
2
|
+
import { buildCreateHypertable, buildEnableCompression, buildCompressionPolicy } from '../timescale/query-builder.js';
|
|
2
3
|
import { readFile, createFile, createDirectory, fileExists } from '@stonyx/utils/file';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import config from 'stonyx/config';
|
|
@@ -97,6 +98,27 @@ export async function generateMigration(description: string = 'migration', confi
|
|
|
97
98
|
downStatements.unshift(`DROP TABLE IF EXISTS "${schemas[name].table}" CASCADE;`);
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Hypertable conversion + compression (TimescaleDB only)
|
|
102
|
+
if (configKey === 'timescale') {
|
|
103
|
+
for (const name of addedOrdered) {
|
|
104
|
+
const schema = schemas[name];
|
|
105
|
+
if (!schema.hypertable) continue;
|
|
106
|
+
|
|
107
|
+
const { timeColumn, chunkInterval } = schema.hypertable;
|
|
108
|
+
upStatements.push(buildCreateHypertable(schema.table, timeColumn, { chunkInterval }).sql + ';');
|
|
109
|
+
|
|
110
|
+
if (schema.hypertable.compress) {
|
|
111
|
+
const { segmentBy, orderBy, after } = schema.hypertable.compress;
|
|
112
|
+
upStatements.push(buildEnableCompression(schema.table, segmentBy, orderBy).sql + ';');
|
|
113
|
+
if (after) {
|
|
114
|
+
upStatements.push(buildCompressionPolicy(schema.table, after).sql + ';');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
downStatements.unshift('-- Hypertable conversion is not reversible; table drop handles cleanup');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
// Removed tables (warn only, commented out)
|
|
101
123
|
for (const name of diff.removedModels) {
|
|
102
124
|
upStatements.push(`-- WARNING: Model '${name}' was removed. Uncomment to drop table:`);
|
|
@@ -5,7 +5,7 @@ import { getPluralName } from '../plural-registry.js';
|
|
|
5
5
|
import { dbKey } from '../db.js';
|
|
6
6
|
import { AggregateProperty } from '../aggregates.js';
|
|
7
7
|
import { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
|
|
8
|
-
import type { ForeignKeyDef, ModelSchema, ViewSchema } from '../types/orm-types.js';
|
|
8
|
+
import type { ForeignKeyDef, HypertableConfig, ModelSchema, ViewSchema } from '../types/orm-types.js';
|
|
9
9
|
import ModelProperty from '../model-property.js';
|
|
10
10
|
|
|
11
11
|
interface ViewSnapshotEntry {
|
|
@@ -24,6 +24,7 @@ interface ModelSnapshotEntry {
|
|
|
24
24
|
columns: Record<string, string>;
|
|
25
25
|
foreignKeys: Record<string, ForeignKeyDef>;
|
|
26
26
|
vectorColumns?: Record<string, number>;
|
|
27
|
+
hypertable?: HypertableConfig;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
interface JoinDef {
|
|
@@ -82,6 +83,8 @@ export function introspectModels(): Record<string, ModelSchema> {
|
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
const hypertable = (modelClass as { hypertable?: HypertableConfig }).hypertable;
|
|
87
|
+
|
|
85
88
|
schemas[name] = {
|
|
86
89
|
table: sanitizeTableName(getPluralName(name)),
|
|
87
90
|
idType,
|
|
@@ -89,6 +92,7 @@ export function introspectModels(): Record<string, ModelSchema> {
|
|
|
89
92
|
foreignKeys,
|
|
90
93
|
relationships,
|
|
91
94
|
vectorColumns,
|
|
95
|
+
hypertable: hypertable || undefined,
|
|
92
96
|
memory: (modelClass as { memory?: boolean }).memory === true,
|
|
93
97
|
};
|
|
94
98
|
}
|
|
@@ -97,13 +101,16 @@ export function introspectModels(): Record<string, ModelSchema> {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Record<string, ModelSchema> = {}): string {
|
|
100
|
-
const { idType, columns, foreignKeys } = schema;
|
|
104
|
+
const { idType, columns, foreignKeys, hypertable } = schema;
|
|
101
105
|
const table = sanitizeTableName(schema.table);
|
|
102
106
|
const lines: string[] = [];
|
|
107
|
+
const useCompositePK = hypertable && idType !== 'string';
|
|
103
108
|
|
|
104
109
|
// Primary key
|
|
105
110
|
if (idType === 'string') {
|
|
106
111
|
lines.push(' "id" VARCHAR(255) PRIMARY KEY');
|
|
112
|
+
} else if (useCompositePK) {
|
|
113
|
+
lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY');
|
|
107
114
|
} else {
|
|
108
115
|
lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY');
|
|
109
116
|
}
|
|
@@ -120,7 +127,11 @@ export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Rec
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
// Timestamps
|
|
123
|
-
|
|
130
|
+
if (useCompositePK) {
|
|
131
|
+
lines.push(' "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()');
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
|
|
134
|
+
}
|
|
124
135
|
lines.push(' "updated_at" TIMESTAMPTZ DEFAULT NOW()');
|
|
125
136
|
|
|
126
137
|
// Foreign key constraints
|
|
@@ -129,6 +140,11 @@ export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Rec
|
|
|
129
140
|
lines.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${refTable}"("${fkDef.column}") ON DELETE SET NULL`);
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
// Composite primary key for hypertable models
|
|
144
|
+
if (useCompositePK) {
|
|
145
|
+
lines.push(` PRIMARY KEY ("id", "${hypertable.timeColumn}")`);
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
return `CREATE TABLE IF NOT EXISTS "${table}" (\n${lines.join(',\n')}\n)`;
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -336,6 +352,7 @@ export function schemasToSnapshot(schemas: Record<string, ModelSchema>): Record<
|
|
|
336
352
|
...(schema.vectorColumns && Object.keys(schema.vectorColumns).length > 0
|
|
337
353
|
? { vectorColumns: { ...schema.vectorColumns } }
|
|
338
354
|
: {}),
|
|
355
|
+
...(schema.hypertable ? { hypertable: schema.hypertable } : {}),
|
|
339
356
|
};
|
|
340
357
|
}
|
|
341
358
|
|
package/src/types/orm-types.ts
CHANGED
|
@@ -86,6 +86,16 @@ export interface ForeignKeyDef {
|
|
|
86
86
|
column: string;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export interface HypertableConfig {
|
|
90
|
+
timeColumn: string;
|
|
91
|
+
chunkInterval?: string;
|
|
92
|
+
compress?: {
|
|
93
|
+
segmentBy?: string;
|
|
94
|
+
orderBy?: string;
|
|
95
|
+
after?: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
export interface ModelSchema {
|
|
90
100
|
table: string;
|
|
91
101
|
idType: string;
|
|
@@ -96,6 +106,7 @@ export interface ModelSchema {
|
|
|
96
106
|
hasMany: Record<string, string | null>;
|
|
97
107
|
};
|
|
98
108
|
vectorColumns?: Record<string, number>;
|
|
109
|
+
hypertable?: HypertableConfig;
|
|
99
110
|
memory: boolean;
|
|
100
111
|
}
|
|
101
112
|
|
|
@@ -139,6 +150,7 @@ export interface SnapshotEntry {
|
|
|
139
150
|
columns?: Record<string, string>;
|
|
140
151
|
foreignKeys?: Record<string, ForeignKeyDef>;
|
|
141
152
|
vectorColumns?: Record<string, number>;
|
|
153
|
+
hypertable?: HypertableConfig;
|
|
142
154
|
isView?: boolean;
|
|
143
155
|
viewName?: string;
|
|
144
156
|
source?: string;
|