@sqldoc/db 0.0.3

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/src/types.ts ADDED
@@ -0,0 +1,308 @@
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
+ fromConnection?: string
39
+ toConnection?: string
40
+ renames?: AtlasRename[]
41
+ }
42
+
43
+ /**
44
+ * A structured schema change description returned from Atlas diff.
45
+ */
46
+ export interface AtlasChange {
47
+ type:
48
+ | 'add_table'
49
+ | 'drop_table'
50
+ | 'rename_table'
51
+ | 'add_column'
52
+ | 'drop_column'
53
+ | 'rename_column'
54
+ | 'modify_column'
55
+ | 'add_index'
56
+ | 'drop_index'
57
+ | 'add_view'
58
+ | 'drop_view'
59
+ | 'add_function'
60
+ | 'drop_function'
61
+ table: string
62
+ name?: string
63
+ detail?: string
64
+ }
65
+
66
+ /**
67
+ * Result returned from Atlas WASI module via stdout.
68
+ * Top-level fields use lowercase json tags.
69
+ */
70
+ export interface AtlasResult {
71
+ schema?: AtlasRealm
72
+ statements?: string[]
73
+ changes?: AtlasChange[]
74
+ renameCandidates?: AtlasRenameCandidate[]
75
+ error?: string
76
+ }
77
+
78
+ // ── Schema types — lowercase (json tags in marshal.go flat types) ───
79
+
80
+ /**
81
+ * A Realm describes a domain of schema resources (physical database instance).
82
+ * Maps to flatRealm in marshal.go.
83
+ */
84
+ export interface AtlasRealm {
85
+ schemas: AtlasSchema[]
86
+ attrs?: AtlasAttr[]
87
+ }
88
+
89
+ /**
90
+ * A Schema describes a named database schema (e.g. "public").
91
+ * Maps to flatSchema in marshal.go.
92
+ */
93
+ export interface AtlasSchema {
94
+ name: string
95
+ tables?: AtlasTable[]
96
+ views?: AtlasView[]
97
+ funcs?: AtlasFunc[]
98
+ procs?: AtlasProc[]
99
+ attrs?: AtlasAttr[]
100
+ }
101
+
102
+ /**
103
+ * A Table represents a table definition.
104
+ * Maps to flatTable in marshal.go.
105
+ */
106
+ export interface AtlasTable {
107
+ name: string
108
+ columns?: AtlasColumn[]
109
+ indexes?: AtlasIndex[]
110
+ primary_key?: AtlasIndex
111
+ foreign_keys?: AtlasForeignKey[]
112
+ attrs?: AtlasAttr[]
113
+ triggers?: AtlasTrigger[]
114
+ }
115
+
116
+ /**
117
+ * A Column represents a column definition.
118
+ * Maps to flatColumn in marshal.go.
119
+ */
120
+ export interface AtlasColumn {
121
+ name: string
122
+ type?: AtlasColumnType
123
+ default?: AtlasExpr
124
+ attrs?: AtlasAttr[]
125
+ }
126
+
127
+ /**
128
+ * ColumnType represents a column type.
129
+ * Maps to flatColumnType in marshal.go.
130
+ * The `T` field is the type name (e.g., "bigint", "text").
131
+ */
132
+ export type TypeCategory =
133
+ | 'string'
134
+ | 'integer'
135
+ | 'float'
136
+ | 'decimal'
137
+ | 'boolean'
138
+ | 'time'
139
+ | 'binary'
140
+ | 'json'
141
+ | 'uuid'
142
+ | 'spatial'
143
+ | 'enum'
144
+ | 'composite'
145
+ | 'array'
146
+ | 'unknown'
147
+
148
+ export interface AtlasColumnType {
149
+ T?: string
150
+ raw?: string
151
+ null?: boolean
152
+ /** Normalized type category — dialect-independent */
153
+ category?: TypeCategory
154
+ /** Whether this is a user-defined type (enum, composite, domain) */
155
+ is_custom?: boolean
156
+ /** Enum values (when category is 'enum') */
157
+ enum_values?: string[]
158
+ /** Composite type fields (when category is 'composite') */
159
+ composite_fields?: Array<{ name: string; type: string }>
160
+ }
161
+
162
+ /**
163
+ * An Index represents an index definition.
164
+ * Maps to flatIndex in marshal.go.
165
+ */
166
+ export interface AtlasIndex {
167
+ name?: string
168
+ unique?: boolean
169
+ parts?: AtlasIndexPart[]
170
+ attrs?: AtlasAttr[]
171
+ }
172
+
173
+ /**
174
+ * An IndexPart represents a single part of an index.
175
+ * Maps to flatIndexPart in marshal.go.
176
+ */
177
+ export interface AtlasIndexPart {
178
+ column?: string
179
+ desc?: boolean
180
+ attrs?: AtlasAttr[]
181
+ }
182
+
183
+ /**
184
+ * A ForeignKey represents a foreign key constraint.
185
+ * Maps to flatForeignKey in marshal.go.
186
+ */
187
+ export interface AtlasForeignKey {
188
+ symbol?: string
189
+ columns?: string[]
190
+ ref_columns?: string[]
191
+ ref_table?: string
192
+ on_update?: string
193
+ on_delete?: string
194
+ }
195
+
196
+ /**
197
+ * A View represents a view definition.
198
+ * Maps to flatView in marshal.go.
199
+ */
200
+ export interface AtlasView {
201
+ name: string
202
+ def?: string
203
+ columns?: AtlasColumn[]
204
+ attrs?: AtlasAttr[]
205
+ }
206
+
207
+ /**
208
+ * A Func represents a function definition.
209
+ * Maps to flatFunc in marshal.go.
210
+ */
211
+ export interface AtlasFunc {
212
+ name: string
213
+ args?: AtlasFuncArg[]
214
+ ret?: AtlasColumnType
215
+ lang?: string
216
+ attrs?: AtlasAttr[]
217
+ }
218
+
219
+ export interface AtlasFuncArg {
220
+ name?: string
221
+ type?: AtlasColumnType
222
+ mode?: string
223
+ }
224
+
225
+ /**
226
+ * A Proc represents a procedure definition.
227
+ * Maps to flatProc in marshal.go.
228
+ */
229
+ export interface AtlasProc {
230
+ name: string
231
+ attrs?: AtlasAttr[]
232
+ }
233
+
234
+ /**
235
+ * A Trigger represents a trigger definition.
236
+ * Maps to flatTrigger in marshal.go.
237
+ */
238
+ export interface AtlasTrigger {
239
+ name: string
240
+ attrs?: AtlasAttr[]
241
+ }
242
+
243
+ /**
244
+ * An expression in schema DDL.
245
+ * Go serializes RawExpr as { X: string }, Literal as { V: string }.
246
+ */
247
+ export type AtlasExpr = { X: string } | { V: string } | unknown
248
+
249
+ // ── Attr union — PascalCase (map[string]string in Go marshal) ───────
250
+
251
+ /**
252
+ * Attrs is a heterogeneous array (Tag, Comment, Check all mixed).
253
+ * We model the known variants as a discriminated union with a fallback.
254
+ * Note: Attr fields use PascalCase (serialized via map[string]string in Go).
255
+ */
256
+ export type AtlasAttr = AtlasTag | AtlasComment | AtlasCheck | Record<string, unknown>
257
+
258
+ /**
259
+ * Tag attr. Serialized as { Name: string, Args: string }.
260
+ * PascalCase because Go marshal uses map[string]string{"Name": ..., "Args": ...}.
261
+ */
262
+ export interface AtlasTag {
263
+ Name: string
264
+ Args: string
265
+ }
266
+
267
+ /**
268
+ * Comment attr. Serialized as { Text: string }.
269
+ */
270
+ export interface AtlasComment {
271
+ Text: string
272
+ }
273
+
274
+ /**
275
+ * Check constraint attr. Serialized as { Name?: string, Expr: string }.
276
+ */
277
+ export interface AtlasCheck {
278
+ Name?: string
279
+ Expr: string
280
+ }
281
+
282
+ // ── Helper functions ────────────────────────────────────────────────
283
+
284
+ /**
285
+ * Type guard: returns true if the given attr is a Tag.
286
+ * Tags have `Name` and `Args` fields but NOT an `Expr` field
287
+ * (which would indicate a Check constraint instead).
288
+ */
289
+ export function isTag(attr: AtlasAttr): attr is AtlasTag {
290
+ return (
291
+ typeof attr === 'object' &&
292
+ attr !== null &&
293
+ 'Name' in attr &&
294
+ typeof (attr as AtlasTag).Name === 'string' &&
295
+ 'Args' in attr &&
296
+ typeof (attr as AtlasTag).Args === 'string' &&
297
+ !('Expr' in attr)
298
+ )
299
+ }
300
+
301
+ /**
302
+ * Extract all Tag attrs from a mixed Attrs array.
303
+ * Returns an empty array if attrs is undefined or empty.
304
+ */
305
+ export function findTags(attrs?: AtlasAttr[]): AtlasTag[] {
306
+ if (!attrs) return []
307
+ return attrs.filter(isTag)
308
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * WASI module instantiation with custom atlas_sql host function import.
3
+ *
4
+ * Includes workarounds for Bun's WASI bugs:
5
+ * 1. FD_MAP ignores stdin/stdout/stderr constructor options
6
+ * 2. random_get returns byte count instead of errno 0
7
+ * 3. proc_exit calls process.exit() instead of throwing
8
+ */
9
+
10
+ import * as crypto from 'node:crypto'
11
+ import * as fs from 'node:fs'
12
+ import * as os from 'node:os'
13
+ import * as path from 'node:path'
14
+ import { WASI } from 'node:wasi'
15
+
16
+ const isBun = typeof (globalThis as any).Bun !== 'undefined'
17
+
18
+ /** Sentinel thrown by Bun proc_exit workaround */
19
+ class WASIExitError {
20
+ code: number
21
+ constructor(code: number) {
22
+ this.code = code
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Get WASI imports compatible with both Node.js and Bun.
28
+ */
29
+ export function getWasiImports(wasi: WASI): Record<string, any> {
30
+ if (typeof (wasi as any).getImportObject === 'function') {
31
+ return (wasi as any).getImportObject()
32
+ }
33
+ if ((wasi as any).wasiImport) {
34
+ return { wasi_snapshot_preview1: (wasi as any).wasiImport }
35
+ }
36
+ throw new Error('Cannot get WASI imports: neither getImportObject() nor wasiImport available')
37
+ }
38
+
39
+ export interface WasiRunOptions {
40
+ stdinData: string
41
+ wasmPath: string
42
+ compiledModule?: WebAssembly.Module
43
+ atlasSqlFn: (reqPtr: number, reqLen: number, respPtr: number, respCap: number) => bigint | number
44
+ onInstance?: (instance: WebAssembly.Instance) => void
45
+ }
46
+
47
+ export interface WasiRunResult {
48
+ stdout: string
49
+ module: WebAssembly.Module
50
+ }
51
+
52
+ export async function runWasi(options: WasiRunOptions): Promise<WasiRunResult> {
53
+ const { stdinData, wasmPath, atlasSqlFn, onInstance } = options
54
+ const uid = crypto.randomUUID()
55
+ const tmpDir = os.tmpdir()
56
+ const stdinPath = path.join(tmpDir, `atlas-stdin-${uid}.json`)
57
+ const stdoutPath = path.join(tmpDir, `atlas-stdout-${uid}.json`)
58
+
59
+ let stdinFd: number | undefined
60
+ let stdoutFd: number | undefined
61
+
62
+ try {
63
+ fs.writeFileSync(stdinPath, stdinData)
64
+ fs.writeFileSync(stdoutPath, '')
65
+
66
+ stdinFd = fs.openSync(stdinPath, 'r')
67
+ stdoutFd = fs.openSync(stdoutPath, 'w')
68
+
69
+ const wasi = new WASI({
70
+ version: 'preview1',
71
+ stdin: stdinFd,
72
+ stdout: stdoutFd,
73
+ returnOnExit: true,
74
+ })
75
+
76
+ // Bun bug #1: FD_MAP ignores constructor options
77
+ if (isBun) {
78
+ const fdMap: Map<number, any> = (wasi as any).FD_MAP
79
+ if (fdMap) {
80
+ const fd0 = fdMap.get(0)
81
+ if (fd0) fd0.real = stdinFd
82
+ const fd1 = fdMap.get(1)
83
+ if (fd1) fd1.real = stdoutFd
84
+ const fd2 = fdMap.get(2)
85
+ if (fd2) fd2.real = 2
86
+ }
87
+ }
88
+
89
+ // Compile module (or reuse cached)
90
+ let wasmModule = options.compiledModule
91
+ if (!wasmModule) {
92
+ const wasmBytes = fs.readFileSync(wasmPath)
93
+ wasmModule = await WebAssembly.compile(wasmBytes)
94
+ }
95
+
96
+ // Build imports
97
+ const wasiImports = getWasiImports(wasi)
98
+ let instanceRef: WebAssembly.Instance | undefined
99
+
100
+ if (isBun && wasiImports.wasi_snapshot_preview1) {
101
+ // Bun bug #2: random_get returns byte count instead of errno 0
102
+ wasiImports.wasi_snapshot_preview1.random_get = (bufPtr: number, bufLen: number) => {
103
+ if (instanceRef) {
104
+ const mem = instanceRef.exports.memory as WebAssembly.Memory
105
+ const view = new Uint8Array(mem.buffer, bufPtr, bufLen)
106
+ crypto.getRandomValues(view)
107
+ }
108
+ return 0
109
+ }
110
+
111
+ // Bun bug #3: proc_exit calls process.exit()
112
+ wasiImports.wasi_snapshot_preview1.proc_exit = (code: number) => {
113
+ throw new WASIExitError(code)
114
+ }
115
+ }
116
+
117
+ const imports = {
118
+ ...wasiImports,
119
+ env: { atlas_sql: atlasSqlFn },
120
+ }
121
+
122
+ // Instantiate
123
+ const instance = await WebAssembly.instantiate(wasmModule, imports)
124
+ instanceRef = instance
125
+
126
+ if (isBun && typeof (wasi as any).setMemory === 'function') {
127
+ ;(wasi as any).setMemory(instance.exports.memory)
128
+ }
129
+
130
+ if (onInstance) {
131
+ onInstance(instance)
132
+ }
133
+
134
+ // Run
135
+ try {
136
+ if (isBun) {
137
+ ;(instance.exports._start as Function)()
138
+ } else {
139
+ wasi.start(instance)
140
+ }
141
+ } catch (err: unknown) {
142
+ if (err instanceof WASIExitError) {
143
+ // Bun sentinel — non-zero exit is fine, we read stdout for error JSON
144
+ } else {
145
+ // Node: returnOnExit throws on non-zero exit code
146
+ const isExitError = err instanceof Error && ('code' in err || err.message.includes('exit'))
147
+ if (!isExitError) throw err
148
+ }
149
+ }
150
+
151
+ // Close fds before reading stdout
152
+ fs.closeSync(stdinFd)
153
+ stdinFd = undefined
154
+ fs.closeSync(stdoutFd)
155
+ stdoutFd = undefined
156
+
157
+ const stdout = fs.readFileSync(stdoutPath, 'utf-8')
158
+ return { stdout, module: wasmModule }
159
+ } finally {
160
+ if (stdinFd !== undefined)
161
+ try {
162
+ fs.closeSync(stdinFd)
163
+ } catch {}
164
+ if (stdoutFd !== undefined)
165
+ try {
166
+ fs.closeSync(stdoutFd)
167
+ } catch {}
168
+ try {
169
+ fs.unlinkSync(stdinPath)
170
+ } catch {}
171
+ try {
172
+ fs.unlinkSync(stdoutPath)
173
+ } catch {}
174
+ }
175
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Worker thread entry point for Atlas WASI execution.
3
+ *
4
+ * This file runs inside a node:worker_threads Worker. It:
5
+ * 1. Receives initialization data via workerData
6
+ * 2. Creates the atlas_sql callback using SharedArrayBuffer + Atomics
7
+ * 3. Runs the WASI module with the stdin data
8
+ * 4. Posts the stdout result back to the parent
9
+ *
10
+ * The atlas_sql callback bridges synchronous WASM calls to the main thread's
11
+ * async database adapter using Atomics.wait (blocking on the worker thread).
12
+ */
13
+
14
+ import { parentPort, workerData } from 'node:worker_threads'
15
+ import { type BridgeBuffers, bridgeRequest, SIGNAL_DONE } from './bridge.ts'
16
+ import { runWasi } from './wasi-host.ts'
17
+
18
+ interface WorkerInit {
19
+ wasmPath: string
20
+ controlBuffer: SharedArrayBuffer
21
+ dataBuffer: SharedArrayBuffer
22
+ stdinData: string
23
+ }
24
+
25
+ const { wasmPath, controlBuffer, dataBuffer, stdinData } = workerData as WorkerInit
26
+
27
+ const buffers: BridgeBuffers = {
28
+ control: controlBuffer,
29
+ data: dataBuffer,
30
+ }
31
+
32
+ // WASM instance memory -- set via onInstance before wasi.start()
33
+ let wasmMemory: WebAssembly.Memory | null = null
34
+
35
+ /**
36
+ * The atlas_sql host function that bridges sync WASM calls
37
+ * to the main thread via SharedArrayBuffer + Atomics.
38
+ *
39
+ * Signature matches Go's //go:wasmimport env atlas_sql:
40
+ * (reqPtr, reqLen, respPtr, respCap) => int64
41
+ *
42
+ * Negative return = buffer too small (abs value = needed size).
43
+ * Positive return = actual response length written.
44
+ */
45
+ function atlasSql(reqPtr: number, reqLen: number, respPtr: number, respCap: number): bigint {
46
+ if (!wasmMemory) {
47
+ throw new Error('WASM memory not yet available in atlas_sql callback')
48
+ }
49
+
50
+ // 1. Read request JSON from WASM memory
51
+ const reqBytes = new Uint8Array(wasmMemory.buffer, reqPtr, reqLen)
52
+ const requestJson = new TextDecoder().decode(reqBytes.slice())
53
+
54
+ // 2. Send request to main thread via bridge and block until response
55
+ const responseJson = bridgeRequest(buffers, requestJson)
56
+
57
+ // 3. Encode response and check capacity
58
+ const respEncoded = new TextEncoder().encode(responseJson)
59
+
60
+ if (respEncoded.byteLength > respCap) {
61
+ return BigInt(-respEncoded.byteLength)
62
+ }
63
+
64
+ // 4. Copy response into WASM memory
65
+ const wasmResp = new Uint8Array(wasmMemory.buffer, respPtr, respCap)
66
+ wasmResp.set(respEncoded)
67
+
68
+ return BigInt(respEncoded.byteLength)
69
+ }
70
+
71
+ async function main(): Promise<void> {
72
+ if (!parentPort) {
73
+ throw new Error('worker.ts must run inside a worker thread')
74
+ }
75
+
76
+ try {
77
+ const result = await runWasi({
78
+ stdinData,
79
+ wasmPath,
80
+ atlasSqlFn: atlasSql,
81
+ onInstance(instance) {
82
+ // Capture WASM memory before execution starts.
83
+ // atlas_sql will use this to read requests from / write responses to WASM memory.
84
+ wasmMemory = instance.exports.memory as WebAssembly.Memory
85
+ },
86
+ })
87
+
88
+ // Signal main thread that WASI execution is done
89
+ const control = new Int32Array(buffers.control)
90
+ Atomics.store(control, 0, SIGNAL_DONE)
91
+ Atomics.notify(control, 0)
92
+
93
+ // Post result back
94
+ parentPort.postMessage({ type: 'result', stdout: result.stdout })
95
+ } catch (err: unknown) {
96
+ // Signal done even on error
97
+ const control = new Int32Array(buffers.control)
98
+ Atomics.store(control, 0, SIGNAL_DONE)
99
+ Atomics.notify(control, 0)
100
+
101
+ const message = err instanceof Error ? err.message : String(err)
102
+ parentPort.postMessage({ type: 'error', error: message })
103
+ }
104
+ }
105
+
106
+ main()
Binary file