@sqldoc/atlas 0.0.1
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/package.json +37 -0
- package/src/__tests__/bridge.test.ts +210 -0
- package/src/__tests__/docker.test.ts +87 -0
- package/src/__tests__/extensions.test.ts +148 -0
- package/src/__tests__/pglite.test.ts +57 -0
- package/src/__tests__/runner.test.ts +151 -0
- package/src/__tests__/types.test.ts +77 -0
- package/src/__tests__/wasi-compat.test.ts +41 -0
- package/src/bridge.ts +152 -0
- package/src/db/docker.ts +91 -0
- package/src/db/pglite.ts +77 -0
- package/src/db/postgres.ts +44 -0
- package/src/db/types.ts +26 -0
- package/src/extensions.ts +116 -0
- package/src/index.ts +97 -0
- package/src/runner.ts +263 -0
- package/src/types.ts +305 -0
- package/src/wasi-host.ts +175 -0
- package/src/worker.ts +106 -0
- package/wasm/atlas.wasm +0 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @sqldoc/atlas -- Atlas WASI integration for sqldoc
|
|
2
|
+
// Schema types, database adapters, and WASI runner
|
|
3
|
+
|
|
4
|
+
import * as fs from 'node:fs'
|
|
5
|
+
import * as path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { createDockerAdapter } from './db/docker.ts'
|
|
8
|
+
import { createPgliteAdapter } from './db/pglite.ts'
|
|
9
|
+
import { createPostgresAdapter } from './db/postgres.ts'
|
|
10
|
+
import { extractExtensions, validatePgliteExtensions, validatePostgresExtensions } from './extensions.ts'
|
|
11
|
+
import { createAtlasRunner } from './runner.ts'
|
|
12
|
+
|
|
13
|
+
export { createDockerAdapter } from './db/docker.ts'
|
|
14
|
+
export { createPgliteAdapter } from './db/pglite.ts'
|
|
15
|
+
export { createPostgresAdapter } from './db/postgres.ts'
|
|
16
|
+
export type { DatabaseAdapter, ExecResult, QueryResult } from './db/types.ts'
|
|
17
|
+
export { extractExtensions, validatePgliteExtensions, validatePostgresExtensions } from './extensions.ts'
|
|
18
|
+
export type { AtlasRunner, AtlasRunnerOptions } from './runner.ts'
|
|
19
|
+
export { createAtlasRunner } from './runner.ts'
|
|
20
|
+
export * from './types.ts'
|
|
21
|
+
|
|
22
|
+
export interface CreateRunnerConfig {
|
|
23
|
+
/** SQL dialect. Default: 'postgres' */
|
|
24
|
+
dialect?: 'postgres' | 'mysql' | 'sqlite'
|
|
25
|
+
/** Database connection URL. If omitted, uses pglite (in-memory postgres). */
|
|
26
|
+
devUrl?: string
|
|
27
|
+
/** SQL file contents to scan for CREATE EXTENSION statements */
|
|
28
|
+
sqlFiles?: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the atlas.wasm binary.
|
|
33
|
+
* Binary mode: ATLAS_WASM_PATH env var set by binary entry point.
|
|
34
|
+
* Dev mode: walk up from current directory to find atlas.wasm.
|
|
35
|
+
*/
|
|
36
|
+
function resolveWasm(): string {
|
|
37
|
+
// Binary mode: WASM path set by binary entry point
|
|
38
|
+
if (process.env.ATLAS_WASM_PATH) {
|
|
39
|
+
return process.env.ATLAS_WASM_PATH
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Dev mode: walk up from current directory to find atlas.wasm
|
|
43
|
+
let dir = path.dirname(fileURLToPath(import.meta.url))
|
|
44
|
+
while (true) {
|
|
45
|
+
for (const candidate of [
|
|
46
|
+
path.join(dir, 'wasm', 'atlas.wasm'),
|
|
47
|
+
path.join(dir, '..', 'wasm', 'atlas.wasm'),
|
|
48
|
+
path.join(dir, 'node_modules', '@sqldoc', 'atlas', 'wasm', 'atlas.wasm'),
|
|
49
|
+
path.join(dir, 'packages', 'atlas', 'wasm', 'atlas.wasm'),
|
|
50
|
+
]) {
|
|
51
|
+
if (fs.existsSync(candidate)) return candidate
|
|
52
|
+
}
|
|
53
|
+
const parent = path.dirname(dir)
|
|
54
|
+
if (parent === dir) break
|
|
55
|
+
dir = parent
|
|
56
|
+
}
|
|
57
|
+
throw new Error(
|
|
58
|
+
'atlas.wasm not found. Set ATLAS_WASM_PATH or build: cd atlas/cmd/atlas-wasi && GOOS=wasip1 GOARCH=wasm go build -o atlas.wasm .',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create an Atlas runner with sensible defaults.
|
|
64
|
+
* Resolves the wasm binary, detects extensions from SQL files,
|
|
65
|
+
* validates them, and creates the appropriate DB adapter.
|
|
66
|
+
*/
|
|
67
|
+
export async function createRunner(config: CreateRunnerConfig = {}): Promise<import('./runner').AtlasRunner> {
|
|
68
|
+
const wasmPath = resolveWasm()
|
|
69
|
+
const dialect = config.dialect ?? 'postgres'
|
|
70
|
+
const devUrl = config.devUrl ?? 'pglite'
|
|
71
|
+
|
|
72
|
+
// Extract extensions from SQL (postgres only)
|
|
73
|
+
const { extensions } =
|
|
74
|
+
dialect === 'postgres' && config.sqlFiles ? extractExtensions(config.sqlFiles) : { extensions: [] }
|
|
75
|
+
|
|
76
|
+
let db: import('./db/types').DatabaseAdapter
|
|
77
|
+
|
|
78
|
+
if (devUrl.startsWith('docker://') || devUrl.startsWith('dockerfile://')) {
|
|
79
|
+
db = await createDockerAdapter(devUrl)
|
|
80
|
+
// Docker postgres — validate extensions are available
|
|
81
|
+
if (extensions.length > 0) {
|
|
82
|
+
await validatePostgresExtensions(extensions, (sql) => db.query(sql))
|
|
83
|
+
}
|
|
84
|
+
} else if (devUrl.startsWith('postgres://') || devUrl.startsWith('postgresql://')) {
|
|
85
|
+
db = await createPostgresAdapter(devUrl)
|
|
86
|
+
// External postgres — validate extensions are available
|
|
87
|
+
if (extensions.length > 0) {
|
|
88
|
+
await validatePostgresExtensions(extensions, (sql) => db.query(sql))
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// pglite — validate and load extensions
|
|
92
|
+
const validExtensions = extensions.length > 0 ? await validatePgliteExtensions(extensions) : []
|
|
93
|
+
db = await createPgliteAdapter(validExtensions.length > 0 ? validExtensions : undefined)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return createAtlasRunner({ wasmPath, db })
|
|
97
|
+
}
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level Atlas runner API.
|
|
3
|
+
*
|
|
4
|
+
* Provides inspect() and diff() functions that:
|
|
5
|
+
* 1. Spawn a worker thread running the Atlas WASI module
|
|
6
|
+
* 2. Handle the SharedArrayBuffer bridge loop on the main thread
|
|
7
|
+
* 3. Execute SQL requests from the WASI module via the DatabaseAdapter
|
|
8
|
+
* 4. Return parsed AtlasResult
|
|
9
|
+
*
|
|
10
|
+
* The compiled WebAssembly.Module is cached across commands.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'node:fs'
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
import * as path from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { Worker } from 'node:worker_threads'
|
|
18
|
+
import {
|
|
19
|
+
type BridgeBuffers,
|
|
20
|
+
bridgeReadRequest,
|
|
21
|
+
bridgeRespond,
|
|
22
|
+
bridgeWaitForSignal,
|
|
23
|
+
createBridgeBuffers,
|
|
24
|
+
SIGNAL_DONE,
|
|
25
|
+
SIGNAL_REQUEST,
|
|
26
|
+
} from './bridge.ts'
|
|
27
|
+
import type { DatabaseAdapter } from './db/types.ts'
|
|
28
|
+
import type { AtlasCommand, AtlasRename, AtlasResult } from './types.ts'
|
|
29
|
+
|
|
30
|
+
export interface AtlasRunnerOptions {
|
|
31
|
+
/** Path to atlas.wasm binary */
|
|
32
|
+
wasmPath: string
|
|
33
|
+
/** Database adapter (pglite or pg) */
|
|
34
|
+
db: DatabaseAdapter
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AtlasRunner {
|
|
38
|
+
/** Execute SQL files and return parsed schema with tags */
|
|
39
|
+
inspect(files: string[], options?: { schema?: string; dialect?: string; fileNames?: string[] }): Promise<AtlasResult>
|
|
40
|
+
|
|
41
|
+
/** Compare two schema states and return migration SQL */
|
|
42
|
+
diff(
|
|
43
|
+
from: string[],
|
|
44
|
+
to: string[],
|
|
45
|
+
options?: { schema?: string; dialect?: string; renames?: AtlasRename[] },
|
|
46
|
+
): Promise<AtlasResult>
|
|
47
|
+
|
|
48
|
+
/** Clean up resources */
|
|
49
|
+
close(): Promise<void>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the path to the worker file (.js or .ts).
|
|
54
|
+
*
|
|
55
|
+
* Prefers compiled .js (production / Bun binary). Falls back to raw .ts
|
|
56
|
+
* for workspace development (vitest, tsx, Node --experimental-strip-types).
|
|
57
|
+
*/
|
|
58
|
+
function resolveWorkerPath(): { workerPath: string; execArgv: string[] } {
|
|
59
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url))
|
|
60
|
+
const candidates = [
|
|
61
|
+
path.resolve(thisDir, 'worker.js'), // dist/worker.js (production)
|
|
62
|
+
path.resolve(thisDir, '../dist/worker.js'), // src/../dist/worker.js (legacy)
|
|
63
|
+
path.resolve(thisDir, 'worker.ts'), // src/worker.ts (raw .ts dev)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const isBun = typeof (globalThis as any).Bun !== 'undefined'
|
|
67
|
+
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (fs.existsSync(candidate)) {
|
|
70
|
+
if (candidate.endsWith('.ts') && !isBun) {
|
|
71
|
+
// Node 22.21+ runs .ts natively — no loader needed for workers
|
|
72
|
+
// Older Node needs tsx/cjs to register TypeScript transform
|
|
73
|
+
const [major, minor] = (process.versions?.node ?? '0.0').split('.').map(Number)
|
|
74
|
+
if (major > 22 || (major === 22 && minor >= 21)) {
|
|
75
|
+
return { workerPath: candidate, execArgv: [] }
|
|
76
|
+
}
|
|
77
|
+
const atlasRequire = createRequire(candidate)
|
|
78
|
+
const tsxCjs = atlasRequire.resolve('tsx/cjs')
|
|
79
|
+
return { workerPath: candidate, execArgv: ['--require', tsxCjs] }
|
|
80
|
+
}
|
|
81
|
+
return { workerPath: candidate, execArgv: [] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error(`Cannot find worker file.\nLooked in:\n${candidates.map((c) => ` ${c}`).join('\n')}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run a single Atlas command via a worker thread.
|
|
90
|
+
*
|
|
91
|
+
* Main thread bridge loop:
|
|
92
|
+
* 1. Wait for worker to signal a request via Atomics
|
|
93
|
+
* 2. Read SQL from shared buffer
|
|
94
|
+
* 3. Execute via DatabaseAdapter
|
|
95
|
+
* 4. Write response to shared buffer
|
|
96
|
+
* 5. Repeat until DONE signal
|
|
97
|
+
*/
|
|
98
|
+
async function runCommand(wasmPath: string, db: DatabaseAdapter, command: AtlasCommand): Promise<AtlasResult> {
|
|
99
|
+
const buffers = createBridgeBuffers()
|
|
100
|
+
const stdinData = JSON.stringify(command)
|
|
101
|
+
const { workerPath, execArgv } = resolveWorkerPath()
|
|
102
|
+
|
|
103
|
+
return new Promise<AtlasResult>((resolve, reject) => {
|
|
104
|
+
const worker = new Worker(workerPath, {
|
|
105
|
+
workerData: {
|
|
106
|
+
wasmPath,
|
|
107
|
+
controlBuffer: buffers.control,
|
|
108
|
+
dataBuffer: buffers.data,
|
|
109
|
+
stdinData,
|
|
110
|
+
},
|
|
111
|
+
execArgv,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
let settled = false
|
|
115
|
+
|
|
116
|
+
worker.on('message', (msg: { type: string; stdout?: string; error?: string }) => {
|
|
117
|
+
if (settled) return
|
|
118
|
+
settled = true
|
|
119
|
+
|
|
120
|
+
if (msg.type === 'error') {
|
|
121
|
+
reject(new Error(`Atlas worker error: ${msg.error}`))
|
|
122
|
+
} else if (msg.type === 'result') {
|
|
123
|
+
try {
|
|
124
|
+
const stdout = (msg.stdout ?? '').trim()
|
|
125
|
+
if (!stdout) {
|
|
126
|
+
resolve({})
|
|
127
|
+
} else {
|
|
128
|
+
resolve(JSON.parse(stdout))
|
|
129
|
+
}
|
|
130
|
+
} catch (_err: unknown) {
|
|
131
|
+
reject(new Error(`Failed to parse Atlas output: ${msg.stdout}`))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
worker.on('error', (err) => {
|
|
137
|
+
if (settled) return
|
|
138
|
+
settled = true
|
|
139
|
+
reject(err)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
worker.on('exit', (code) => {
|
|
143
|
+
if (settled) return
|
|
144
|
+
settled = true
|
|
145
|
+
if (code !== 0) {
|
|
146
|
+
reject(new Error(`Atlas worker exited with code ${code}`))
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Start the bridge loop (runs on main thread, async)
|
|
151
|
+
handleBridgeLoop(buffers, db).catch((err) => {
|
|
152
|
+
if (!settled) {
|
|
153
|
+
settled = true
|
|
154
|
+
worker.terminate()
|
|
155
|
+
reject(err)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Main-thread bridge loop: handles atlas_sql requests from the worker.
|
|
163
|
+
* Runs until the worker signals DONE.
|
|
164
|
+
*/
|
|
165
|
+
async function handleBridgeLoop(buffers: BridgeBuffers, db: DatabaseAdapter): Promise<void> {
|
|
166
|
+
while (true) {
|
|
167
|
+
const signal = await bridgeWaitForSignal(buffers)
|
|
168
|
+
|
|
169
|
+
if (signal === SIGNAL_DONE) {
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (signal !== SIGNAL_REQUEST) {
|
|
174
|
+
// Unexpected signal -- wait again
|
|
175
|
+
// Reset to idle so bridgeWaitForSignal can poll again
|
|
176
|
+
const _control = new Int32Array(buffers.control)
|
|
177
|
+
// If it's RESPONSE, the worker hasn't reset yet. Wait briefly.
|
|
178
|
+
await new Promise((r) => setTimeout(r, 1))
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Read SQL request from shared buffer
|
|
183
|
+
const reqJson = bridgeReadRequest(buffers)
|
|
184
|
+
|
|
185
|
+
let response: Record<string, unknown>
|
|
186
|
+
try {
|
|
187
|
+
const req = JSON.parse(reqJson) as { type: string; sql: string; args?: unknown[] }
|
|
188
|
+
|
|
189
|
+
if (req.type === 'query') {
|
|
190
|
+
const result = await db.query(req.sql, req.args)
|
|
191
|
+
response = { columns: result.columns, rows: result.rows }
|
|
192
|
+
} else {
|
|
193
|
+
const result = await db.exec(req.sql, req.args)
|
|
194
|
+
response = { rows_affected: result.rowsAffected }
|
|
195
|
+
}
|
|
196
|
+
} catch (err: unknown) {
|
|
197
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
198
|
+
response = { error: message }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Write response and notify worker (handle BigInt from pglite)
|
|
202
|
+
bridgeRespond(
|
|
203
|
+
buffers,
|
|
204
|
+
JSON.stringify(response, (_k, v) => (typeof v === 'bigint' ? v.toString() : v)),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// After responding, the worker will reset signal to IDLE.
|
|
208
|
+
// We need to wait for it to do so before calling bridgeWaitForSignal again.
|
|
209
|
+
// Small yield to allow worker to process.
|
|
210
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create an AtlasRunner instance.
|
|
216
|
+
*
|
|
217
|
+
* The runner caches the compiled WebAssembly.Module (compiles atlas.wasm once).
|
|
218
|
+
* Each inspect/diff call spawns a worker thread that reuses the cached module.
|
|
219
|
+
*/
|
|
220
|
+
export async function createAtlasRunner(options: AtlasRunnerOptions): Promise<AtlasRunner> {
|
|
221
|
+
const { wasmPath, db } = options
|
|
222
|
+
|
|
223
|
+
// Verify wasm file exists
|
|
224
|
+
if (!fs.existsSync(wasmPath)) {
|
|
225
|
+
throw new Error(`Atlas WASM binary not found: ${wasmPath}`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
async inspect(
|
|
230
|
+
files: string[],
|
|
231
|
+
opts?: { schema?: string; dialect?: string; fileNames?: string[] },
|
|
232
|
+
): Promise<AtlasResult> {
|
|
233
|
+
const command: AtlasCommand = {
|
|
234
|
+
type: 'inspect',
|
|
235
|
+
dialect: (opts?.dialect as AtlasCommand['dialect']) ?? 'postgres',
|
|
236
|
+
files,
|
|
237
|
+
fileNames: opts?.fileNames,
|
|
238
|
+
schema: opts?.schema,
|
|
239
|
+
}
|
|
240
|
+
return runCommand(wasmPath, db, command)
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async diff(
|
|
244
|
+
from: string[],
|
|
245
|
+
to: string[],
|
|
246
|
+
opts?: { schema?: string; dialect?: string; renames?: AtlasRename[] },
|
|
247
|
+
): Promise<AtlasResult> {
|
|
248
|
+
const command: AtlasCommand = {
|
|
249
|
+
type: 'diff',
|
|
250
|
+
dialect: (opts?.dialect as AtlasCommand['dialect']) ?? 'postgres',
|
|
251
|
+
from,
|
|
252
|
+
to,
|
|
253
|
+
schema: opts?.schema,
|
|
254
|
+
renames: opts?.renames,
|
|
255
|
+
}
|
|
256
|
+
return runCommand(wasmPath, db, command)
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async close(): Promise<void> {
|
|
260
|
+
await db.close()
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// ── Atlas WASI command/result types ─────────────────────────────────
|
|
2
|
+
// Hand-crafted from atlas/cmd/atlas-wasi/marshal.go (cycle-free flat types).
|
|
3
|
+
// Schema fields use lowercase json tags from the flat marshaler.
|
|
4
|
+
// Attr variants (Tag, Comment, Check) use PascalCase from map[string]string.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A known rename (from @docs.previously tags) to pass to Atlas before diffing.
|
|
8
|
+
*/
|
|
9
|
+
export interface AtlasRename {
|
|
10
|
+
type: 'column' | 'table'
|
|
11
|
+
table: string
|
|
12
|
+
oldName: string
|
|
13
|
+
newName: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A potential rename detected by Atlas during diff (drop+add pair with same type).
|
|
18
|
+
*/
|
|
19
|
+
export interface AtlasRenameCandidate {
|
|
20
|
+
type: 'column' | 'table'
|
|
21
|
+
table: string
|
|
22
|
+
oldName: string
|
|
23
|
+
newName: string
|
|
24
|
+
colType?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Command sent to Atlas WASI module via stdin.
|
|
29
|
+
*/
|
|
30
|
+
export interface AtlasCommand {
|
|
31
|
+
type: 'inspect' | 'diff' | 'apply'
|
|
32
|
+
dialect: 'postgres' | 'mysql' | 'sqlite'
|
|
33
|
+
schema?: string
|
|
34
|
+
files?: string[]
|
|
35
|
+
fileNames?: string[]
|
|
36
|
+
from?: string[]
|
|
37
|
+
to?: string[]
|
|
38
|
+
renames?: AtlasRename[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A structured schema change description returned from Atlas diff.
|
|
43
|
+
*/
|
|
44
|
+
export interface AtlasChange {
|
|
45
|
+
type:
|
|
46
|
+
| 'add_table'
|
|
47
|
+
| 'drop_table'
|
|
48
|
+
| 'rename_table'
|
|
49
|
+
| 'add_column'
|
|
50
|
+
| 'drop_column'
|
|
51
|
+
| 'rename_column'
|
|
52
|
+
| 'modify_column'
|
|
53
|
+
| 'add_index'
|
|
54
|
+
| 'drop_index'
|
|
55
|
+
| 'add_view'
|
|
56
|
+
| 'drop_view'
|
|
57
|
+
| 'add_function'
|
|
58
|
+
| 'drop_function'
|
|
59
|
+
table: string
|
|
60
|
+
name?: string
|
|
61
|
+
detail?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result returned from Atlas WASI module via stdout.
|
|
66
|
+
* Top-level fields use lowercase json tags.
|
|
67
|
+
*/
|
|
68
|
+
export interface AtlasResult {
|
|
69
|
+
schema?: AtlasRealm
|
|
70
|
+
statements?: string[]
|
|
71
|
+
changes?: AtlasChange[]
|
|
72
|
+
renameCandidates?: AtlasRenameCandidate[]
|
|
73
|
+
error?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Schema types — lowercase (json tags in marshal.go flat types) ───
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A Realm describes a domain of schema resources (physical database instance).
|
|
80
|
+
* Maps to flatRealm in marshal.go.
|
|
81
|
+
*/
|
|
82
|
+
export interface AtlasRealm {
|
|
83
|
+
schemas: AtlasSchema[]
|
|
84
|
+
attrs?: AtlasAttr[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A Schema describes a named database schema (e.g. "public").
|
|
89
|
+
* Maps to flatSchema in marshal.go.
|
|
90
|
+
*/
|
|
91
|
+
export interface AtlasSchema {
|
|
92
|
+
name: string
|
|
93
|
+
tables?: AtlasTable[]
|
|
94
|
+
views?: AtlasView[]
|
|
95
|
+
funcs?: AtlasFunc[]
|
|
96
|
+
procs?: AtlasProc[]
|
|
97
|
+
attrs?: AtlasAttr[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A Table represents a table definition.
|
|
102
|
+
* Maps to flatTable in marshal.go.
|
|
103
|
+
*/
|
|
104
|
+
export interface AtlasTable {
|
|
105
|
+
name: string
|
|
106
|
+
columns?: AtlasColumn[]
|
|
107
|
+
indexes?: AtlasIndex[]
|
|
108
|
+
primary_key?: AtlasIndex
|
|
109
|
+
foreign_keys?: AtlasForeignKey[]
|
|
110
|
+
attrs?: AtlasAttr[]
|
|
111
|
+
triggers?: AtlasTrigger[]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A Column represents a column definition.
|
|
116
|
+
* Maps to flatColumn in marshal.go.
|
|
117
|
+
*/
|
|
118
|
+
export interface AtlasColumn {
|
|
119
|
+
name: string
|
|
120
|
+
type?: AtlasColumnType
|
|
121
|
+
default?: AtlasExpr
|
|
122
|
+
attrs?: AtlasAttr[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* ColumnType represents a column type.
|
|
127
|
+
* Maps to flatColumnType in marshal.go.
|
|
128
|
+
* The `T` field is the type name (e.g., "bigint", "text").
|
|
129
|
+
*/
|
|
130
|
+
export type TypeCategory =
|
|
131
|
+
| 'string'
|
|
132
|
+
| 'integer'
|
|
133
|
+
| 'float'
|
|
134
|
+
| 'decimal'
|
|
135
|
+
| 'boolean'
|
|
136
|
+
| 'time'
|
|
137
|
+
| 'binary'
|
|
138
|
+
| 'json'
|
|
139
|
+
| 'uuid'
|
|
140
|
+
| 'spatial'
|
|
141
|
+
| 'enum'
|
|
142
|
+
| 'composite'
|
|
143
|
+
| 'unknown'
|
|
144
|
+
|
|
145
|
+
export interface AtlasColumnType {
|
|
146
|
+
T?: string
|
|
147
|
+
raw?: string
|
|
148
|
+
null?: boolean
|
|
149
|
+
/** Normalized type category — dialect-independent */
|
|
150
|
+
category?: TypeCategory
|
|
151
|
+
/** Whether this is a user-defined type (enum, composite, domain) */
|
|
152
|
+
is_custom?: boolean
|
|
153
|
+
/** Enum values (when category is 'enum') */
|
|
154
|
+
enum_values?: string[]
|
|
155
|
+
/** Composite type fields (when category is 'composite') */
|
|
156
|
+
composite_fields?: Array<{ name: string; type: string }>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* An Index represents an index definition.
|
|
161
|
+
* Maps to flatIndex in marshal.go.
|
|
162
|
+
*/
|
|
163
|
+
export interface AtlasIndex {
|
|
164
|
+
name?: string
|
|
165
|
+
unique?: boolean
|
|
166
|
+
parts?: AtlasIndexPart[]
|
|
167
|
+
attrs?: AtlasAttr[]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* An IndexPart represents a single part of an index.
|
|
172
|
+
* Maps to flatIndexPart in marshal.go.
|
|
173
|
+
*/
|
|
174
|
+
export interface AtlasIndexPart {
|
|
175
|
+
column?: string
|
|
176
|
+
desc?: boolean
|
|
177
|
+
attrs?: AtlasAttr[]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A ForeignKey represents a foreign key constraint.
|
|
182
|
+
* Maps to flatForeignKey in marshal.go.
|
|
183
|
+
*/
|
|
184
|
+
export interface AtlasForeignKey {
|
|
185
|
+
symbol?: string
|
|
186
|
+
columns?: string[]
|
|
187
|
+
ref_columns?: string[]
|
|
188
|
+
ref_table?: string
|
|
189
|
+
on_update?: string
|
|
190
|
+
on_delete?: string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A View represents a view definition.
|
|
195
|
+
* Maps to flatView in marshal.go.
|
|
196
|
+
*/
|
|
197
|
+
export interface AtlasView {
|
|
198
|
+
name: string
|
|
199
|
+
def?: string
|
|
200
|
+
columns?: AtlasColumn[]
|
|
201
|
+
attrs?: AtlasAttr[]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A Func represents a function definition.
|
|
206
|
+
* Maps to flatFunc in marshal.go.
|
|
207
|
+
*/
|
|
208
|
+
export interface AtlasFunc {
|
|
209
|
+
name: string
|
|
210
|
+
args?: AtlasFuncArg[]
|
|
211
|
+
ret?: AtlasColumnType
|
|
212
|
+
lang?: string
|
|
213
|
+
attrs?: AtlasAttr[]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface AtlasFuncArg {
|
|
217
|
+
name?: string
|
|
218
|
+
type?: AtlasColumnType
|
|
219
|
+
mode?: string
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A Proc represents a procedure definition.
|
|
224
|
+
* Maps to flatProc in marshal.go.
|
|
225
|
+
*/
|
|
226
|
+
export interface AtlasProc {
|
|
227
|
+
name: string
|
|
228
|
+
attrs?: AtlasAttr[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A Trigger represents a trigger definition.
|
|
233
|
+
* Maps to flatTrigger in marshal.go.
|
|
234
|
+
*/
|
|
235
|
+
export interface AtlasTrigger {
|
|
236
|
+
name: string
|
|
237
|
+
attrs?: AtlasAttr[]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* An expression in schema DDL.
|
|
242
|
+
* Go serializes RawExpr as { X: string }, Literal as { V: string }.
|
|
243
|
+
*/
|
|
244
|
+
export type AtlasExpr = { X: string } | { V: string } | unknown
|
|
245
|
+
|
|
246
|
+
// ── Attr union — PascalCase (map[string]string in Go marshal) ───────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Attrs is a heterogeneous array (Tag, Comment, Check all mixed).
|
|
250
|
+
* We model the known variants as a discriminated union with a fallback.
|
|
251
|
+
* Note: Attr fields use PascalCase (serialized via map[string]string in Go).
|
|
252
|
+
*/
|
|
253
|
+
export type AtlasAttr = AtlasTag | AtlasComment | AtlasCheck | Record<string, unknown>
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Tag attr. Serialized as { Name: string, Args: string }.
|
|
257
|
+
* PascalCase because Go marshal uses map[string]string{"Name": ..., "Args": ...}.
|
|
258
|
+
*/
|
|
259
|
+
export interface AtlasTag {
|
|
260
|
+
Name: string
|
|
261
|
+
Args: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Comment attr. Serialized as { Text: string }.
|
|
266
|
+
*/
|
|
267
|
+
export interface AtlasComment {
|
|
268
|
+
Text: string
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Check constraint attr. Serialized as { Name?: string, Expr: string }.
|
|
273
|
+
*/
|
|
274
|
+
export interface AtlasCheck {
|
|
275
|
+
Name?: string
|
|
276
|
+
Expr: string
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Helper functions ────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Type guard: returns true if the given attr is a Tag.
|
|
283
|
+
* Tags have `Name` and `Args` fields but NOT an `Expr` field
|
|
284
|
+
* (which would indicate a Check constraint instead).
|
|
285
|
+
*/
|
|
286
|
+
export function isTag(attr: AtlasAttr): attr is AtlasTag {
|
|
287
|
+
return (
|
|
288
|
+
typeof attr === 'object' &&
|
|
289
|
+
attr !== null &&
|
|
290
|
+
'Name' in attr &&
|
|
291
|
+
typeof (attr as AtlasTag).Name === 'string' &&
|
|
292
|
+
'Args' in attr &&
|
|
293
|
+
typeof (attr as AtlasTag).Args === 'string' &&
|
|
294
|
+
!('Expr' in attr)
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Extract all Tag attrs from a mixed Attrs array.
|
|
300
|
+
* Returns an empty array if attrs is undefined or empty.
|
|
301
|
+
*/
|
|
302
|
+
export function findTags(attrs?: AtlasAttr[]): AtlasTag[] {
|
|
303
|
+
if (!attrs) return []
|
|
304
|
+
return attrs.filter(isTag)
|
|
305
|
+
}
|