@memberjunction/sqlglot-ts 0.0.1 → 5.5.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 +72 -28
- package/dist/SqlGlotClient.d.ts +80 -0
- package/dist/SqlGlotClient.d.ts.map +1 -0
- package/dist/SqlGlotClient.js +332 -0
- package/dist/SqlGlotClient.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +33 -6
- package/src/python/server.py +188 -0
package/README.md
CHANGED
|
@@ -1,45 +1,89 @@
|
|
|
1
1
|
# @memberjunction/sqlglot-ts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript wrapper for Python's [sqlglot](https://github.com/tobymao/sqlglot) SQL transpiler. Provides deterministic, verifiable SQL dialect conversion via a managed local Python FastAPI microservice.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **31 SQL dialects** supported (T-SQL, PostgreSQL, MySQL, Snowflake, BigQuery, etc.)
|
|
8
|
+
- **Deterministic conversion** — no LLM randomness, reproducible results
|
|
9
|
+
- **Zero MemberJunction dependencies** — standalone community package
|
|
10
|
+
- **Ephemeral port binding** — microservice runs on `127.0.0.1` with auto-assigned port
|
|
11
|
+
- **Managed lifecycle** — automatic startup/shutdown of the Python process
|
|
12
|
+
- **Statement-by-statement mode** — individual statement tracking, one failure doesn't block others
|
|
8
13
|
|
|
9
|
-
##
|
|
14
|
+
## Prerequisites
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@memberjunction/sqlglot-ts`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
16
|
+
- **Python 3.9+** with `sqlglot`, `fastapi`, and `uvicorn` installed:
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
```bash
|
|
19
|
+
pip install sqlglot fastapi uvicorn
|
|
20
|
+
```
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
## Usage
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
```typescript
|
|
25
|
+
import { SqlGlotClient } from '@memberjunction/sqlglot-ts';
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
const client = new SqlGlotClient();
|
|
28
|
+
await client.start();
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
// Transpile T-SQL to PostgreSQL
|
|
31
|
+
const result = await client.transpile(
|
|
32
|
+
"SELECT ISNULL(col, 0) FROM [dbo].[MyTable]",
|
|
33
|
+
{ fromDialect: 'tsql', toDialect: 'postgres' }
|
|
34
|
+
);
|
|
35
|
+
console.log(result.sql);
|
|
36
|
+
// Output: SELECT COALESCE(col, 0) FROM "dbo"."MyTable";
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
// Statement-by-statement mode
|
|
39
|
+
const stmtResult = await client.transpileStatements(
|
|
40
|
+
"SELECT TOP 10 * FROM Users; SELECT GETDATE();",
|
|
41
|
+
{ fromDialect: 'tsql', toDialect: 'postgres' }
|
|
42
|
+
);
|
|
43
|
+
console.log(stmtResult.statements);
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
// Parse SQL to AST
|
|
46
|
+
const ast = await client.parse(
|
|
47
|
+
"SELECT 1",
|
|
48
|
+
{ dialect: 'postgres' }
|
|
49
|
+
);
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
// List supported dialects
|
|
52
|
+
const dialects = await client.getDialects();
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
54
|
+
// Health check
|
|
55
|
+
const health = await client.health();
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
await client.stop();
|
|
58
|
+
```
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### `SqlGlotClient`
|
|
63
|
+
|
|
64
|
+
#### Constructor Options
|
|
65
|
+
|
|
66
|
+
| Option | Type | Default | Description |
|
|
67
|
+
|--------|------|---------|-------------|
|
|
68
|
+
| `pythonPath` | `string` | `'python3'` | Path to Python executable |
|
|
69
|
+
| `serverPath` | `string` | auto-detected | Path to `server.py` |
|
|
70
|
+
| `startupTimeoutMs` | `number` | `30000` | Max ms to wait for server startup |
|
|
71
|
+
| `requestTimeoutMs` | `number` | `60000` | Max ms per HTTP request |
|
|
72
|
+
|
|
73
|
+
#### Methods
|
|
74
|
+
|
|
75
|
+
- `start()` — Start the Python microservice
|
|
76
|
+
- `stop()` — Stop the Python microservice
|
|
77
|
+
- `transpile(sql, options)` — Transpile SQL between dialects (batch)
|
|
78
|
+
- `transpileStatements(sql, options)` — Transpile statement-by-statement
|
|
79
|
+
- `parse(sql, options)` — Parse SQL to AST (JSON)
|
|
80
|
+
- `getDialects()` — List all supported dialects
|
|
81
|
+
- `health()` — Server health check
|
|
82
|
+
|
|
83
|
+
## Attribution
|
|
84
|
+
|
|
85
|
+
This package wraps the excellent [sqlglot](https://github.com/tobymao/sqlglot) Python library by Toby Mao and contributors. sqlglot provides a comprehensive SQL parser, transpiler, and optimizer supporting 31 SQL dialects with 8,900+ stars and 7,000+ commits.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
ISC
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SqlGlotClientOptions, TranspileOptions, TranspileResult, ParseOptions, ParseResult, HealthStatus } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript client for the sqlglot Python microservice.
|
|
4
|
+
*
|
|
5
|
+
* Spawns a Python FastAPI process on 127.0.0.1 with an ephemeral port,
|
|
6
|
+
* then communicates via HTTP. The lifecycle methods `start()` and `stop()`
|
|
7
|
+
* manage the child process. Cleanup handlers are registered for SIGINT/SIGTERM.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const client = new SqlGlotClient();
|
|
12
|
+
* await client.start();
|
|
13
|
+
*
|
|
14
|
+
* const result = await client.transpile({
|
|
15
|
+
* sql: "SELECT ISNULL(col, 0) FROM [dbo].[MyTable]",
|
|
16
|
+
* fromDialect: 'tsql',
|
|
17
|
+
* toDialect: 'postgres',
|
|
18
|
+
* });
|
|
19
|
+
* console.log(result.sql);
|
|
20
|
+
*
|
|
21
|
+
* await client.stop();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class SqlGlotClient {
|
|
25
|
+
private process;
|
|
26
|
+
private port;
|
|
27
|
+
private readonly pythonPath;
|
|
28
|
+
private readonly serverPath;
|
|
29
|
+
private readonly startupTimeoutMs;
|
|
30
|
+
private readonly requestTimeoutMs;
|
|
31
|
+
private stopping;
|
|
32
|
+
private cleanupRegistered;
|
|
33
|
+
constructor(options?: SqlGlotClientOptions);
|
|
34
|
+
/** Whether the Python microservice is currently running */
|
|
35
|
+
get IsRunning(): boolean;
|
|
36
|
+
/** The port the Python microservice is listening on, or null if not running */
|
|
37
|
+
get Port(): number | null;
|
|
38
|
+
/**
|
|
39
|
+
* Start the Python microservice. Resolves once the server is ready.
|
|
40
|
+
* If already running, this is a no-op.
|
|
41
|
+
*/
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Stop the Python microservice. Resolves once the process has exited.
|
|
45
|
+
* If not running, this is a no-op.
|
|
46
|
+
*/
|
|
47
|
+
stop(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Transpile SQL from one dialect to another.
|
|
50
|
+
* All statements are transpiled together as a batch.
|
|
51
|
+
*/
|
|
52
|
+
transpile(sql: string, options: TranspileOptions): Promise<TranspileResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Transpile SQL statement-by-statement.
|
|
55
|
+
* Each statement is transpiled individually, so one failure doesn't block others.
|
|
56
|
+
*/
|
|
57
|
+
transpileStatements(sql: string, options: TranspileOptions): Promise<TranspileResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Parse SQL and return the AST as JSON.
|
|
60
|
+
*/
|
|
61
|
+
parse(sql: string, options: ParseOptions): Promise<ParseResult>;
|
|
62
|
+
/**
|
|
63
|
+
* List all supported SQL dialects.
|
|
64
|
+
*/
|
|
65
|
+
getDialects(): Promise<string[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Check server health and return status information.
|
|
68
|
+
*/
|
|
69
|
+
health(): Promise<HealthStatus>;
|
|
70
|
+
private assertRunning;
|
|
71
|
+
/**
|
|
72
|
+
* Poll the health endpoint until the server is accepting requests.
|
|
73
|
+
* Uses short intervals with an overall timeout from startupTimeoutMs.
|
|
74
|
+
*/
|
|
75
|
+
private waitForReady;
|
|
76
|
+
private registerCleanup;
|
|
77
|
+
private httpPost;
|
|
78
|
+
private httpGet;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=SqlGlotClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SqlGlotClient.d.ts","sourceRoot":"","sources":["../src/SqlGlotClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAC;AAyBpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,OAAO,CAAC,EAAE,oBAAoB;IAO1C,2DAA2D;IAC3D,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,+EAA+E;IAC/E,IAAI,IAAI,IAAI,MAAM,GAAG,IAAI,CAExB;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyE5B;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B;;;OAGG;IACG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAWjF;;;OAGG;IACG,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAW3F;;OAEG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAQrE;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAMtC;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAerC,OAAO,CAAC,aAAa;IAQrB;;;OAGG;YACW,YAAY;IAc1B,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,QAAQ;IA6ChB,OAAO,CAAC,OAAO;CAsChB"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the Python server script path.
|
|
10
|
+
* When running from dist/ the path is ../src/python/server.py
|
|
11
|
+
* When running from src/ (e.g. vitest) the path is ./python/server.py
|
|
12
|
+
*/
|
|
13
|
+
function resolveServerPath() {
|
|
14
|
+
const candidates = [
|
|
15
|
+
path.resolve(__dirname, '..', 'src', 'python', 'server.py'), // from dist/
|
|
16
|
+
path.resolve(__dirname, 'python', 'server.py'), // from src/ (vitest)
|
|
17
|
+
];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (existsSync(candidate)) {
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return candidates[0]; // fallback; will fail at spawn time with a clear error
|
|
24
|
+
}
|
|
25
|
+
const DEFAULT_SERVER_PATH = resolveServerPath();
|
|
26
|
+
/**
|
|
27
|
+
* TypeScript client for the sqlglot Python microservice.
|
|
28
|
+
*
|
|
29
|
+
* Spawns a Python FastAPI process on 127.0.0.1 with an ephemeral port,
|
|
30
|
+
* then communicates via HTTP. The lifecycle methods `start()` and `stop()`
|
|
31
|
+
* manage the child process. Cleanup handlers are registered for SIGINT/SIGTERM.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const client = new SqlGlotClient();
|
|
36
|
+
* await client.start();
|
|
37
|
+
*
|
|
38
|
+
* const result = await client.transpile({
|
|
39
|
+
* sql: "SELECT ISNULL(col, 0) FROM [dbo].[MyTable]",
|
|
40
|
+
* fromDialect: 'tsql',
|
|
41
|
+
* toDialect: 'postgres',
|
|
42
|
+
* });
|
|
43
|
+
* console.log(result.sql);
|
|
44
|
+
*
|
|
45
|
+
* await client.stop();
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export class SqlGlotClient {
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.process = null;
|
|
51
|
+
this.port = null;
|
|
52
|
+
this.stopping = false;
|
|
53
|
+
this.cleanupRegistered = false;
|
|
54
|
+
this.pythonPath = options?.pythonPath ?? 'python3';
|
|
55
|
+
this.serverPath = options?.serverPath ?? DEFAULT_SERVER_PATH;
|
|
56
|
+
this.startupTimeoutMs = options?.startupTimeoutMs ?? 30000;
|
|
57
|
+
this.requestTimeoutMs = options?.requestTimeoutMs ?? 60000;
|
|
58
|
+
}
|
|
59
|
+
/** Whether the Python microservice is currently running */
|
|
60
|
+
get IsRunning() {
|
|
61
|
+
return this.process !== null && this.port !== null && !this.stopping;
|
|
62
|
+
}
|
|
63
|
+
/** The port the Python microservice is listening on, or null if not running */
|
|
64
|
+
get Port() {
|
|
65
|
+
return this.port;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Start the Python microservice. Resolves once the server is ready.
|
|
69
|
+
* If already running, this is a no-op.
|
|
70
|
+
*/
|
|
71
|
+
async start() {
|
|
72
|
+
if (this.IsRunning) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.stopping = false;
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
const proc = spawn(this.pythonPath, [this.serverPath, '0'], {
|
|
78
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
79
|
+
env: { ...process.env },
|
|
80
|
+
});
|
|
81
|
+
let stdoutBuffer = '';
|
|
82
|
+
let stderrBuffer = '';
|
|
83
|
+
let resolved = false;
|
|
84
|
+
const timeout = setTimeout(() => {
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
resolved = true;
|
|
87
|
+
proc.kill('SIGKILL');
|
|
88
|
+
reject(new Error(`sqlglot-ts server failed to start within ${this.startupTimeoutMs}ms. ` +
|
|
89
|
+
`stderr: ${stderrBuffer.slice(0, 500)}`));
|
|
90
|
+
}
|
|
91
|
+
}, this.startupTimeoutMs);
|
|
92
|
+
proc.stdout.on('data', (chunk) => {
|
|
93
|
+
stdoutBuffer += chunk.toString();
|
|
94
|
+
const match = stdoutBuffer.match(/SQLGLOT_PORT=(\d+)/);
|
|
95
|
+
if (match && !resolved) {
|
|
96
|
+
resolved = true;
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
this.port = parseInt(match[1], 10);
|
|
99
|
+
this.process = proc;
|
|
100
|
+
this.registerCleanup();
|
|
101
|
+
// Wait for the server to be ready before resolving
|
|
102
|
+
this.waitForReady()
|
|
103
|
+
.then(() => resolve())
|
|
104
|
+
.catch((err) => reject(err));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
proc.stderr.on('data', (chunk) => {
|
|
108
|
+
stderrBuffer += chunk.toString();
|
|
109
|
+
});
|
|
110
|
+
proc.on('error', (err) => {
|
|
111
|
+
if (!resolved) {
|
|
112
|
+
resolved = true;
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
reject(new Error(`Failed to spawn Python process: ${err.message}`));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
proc.on('exit', (code) => {
|
|
118
|
+
if (!resolved) {
|
|
119
|
+
resolved = true;
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
reject(new Error(`Python process exited with code ${code} before becoming ready. ` +
|
|
122
|
+
`stderr: ${stderrBuffer.slice(0, 500)}`));
|
|
123
|
+
}
|
|
124
|
+
// If we were running and the process dies unexpectedly, clean up
|
|
125
|
+
if (this.process === proc) {
|
|
126
|
+
this.process = null;
|
|
127
|
+
this.port = null;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stop the Python microservice. Resolves once the process has exited.
|
|
134
|
+
* If not running, this is a no-op.
|
|
135
|
+
*/
|
|
136
|
+
async stop() {
|
|
137
|
+
if (!this.process) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.stopping = true;
|
|
141
|
+
const proc = this.process;
|
|
142
|
+
this.process = null;
|
|
143
|
+
this.port = null;
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const timeout = setTimeout(() => {
|
|
146
|
+
proc.kill('SIGKILL');
|
|
147
|
+
resolve();
|
|
148
|
+
}, 5000);
|
|
149
|
+
proc.on('exit', () => {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
resolve();
|
|
152
|
+
});
|
|
153
|
+
proc.kill('SIGTERM');
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Transpile SQL from one dialect to another.
|
|
158
|
+
* All statements are transpiled together as a batch.
|
|
159
|
+
*/
|
|
160
|
+
async transpile(sql, options) {
|
|
161
|
+
this.assertRunning();
|
|
162
|
+
return this.httpPost('/transpile', {
|
|
163
|
+
sql,
|
|
164
|
+
from_dialect: options.fromDialect,
|
|
165
|
+
to_dialect: options.toDialect,
|
|
166
|
+
pretty: options.pretty ?? true,
|
|
167
|
+
error_level: options.errorLevel ?? 'WARN',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Transpile SQL statement-by-statement.
|
|
172
|
+
* Each statement is transpiled individually, so one failure doesn't block others.
|
|
173
|
+
*/
|
|
174
|
+
async transpileStatements(sql, options) {
|
|
175
|
+
this.assertRunning();
|
|
176
|
+
return this.httpPost('/transpile-statements', {
|
|
177
|
+
sql,
|
|
178
|
+
from_dialect: options.fromDialect,
|
|
179
|
+
to_dialect: options.toDialect,
|
|
180
|
+
pretty: options.pretty ?? true,
|
|
181
|
+
error_level: options.errorLevel ?? 'WARN',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Parse SQL and return the AST as JSON.
|
|
186
|
+
*/
|
|
187
|
+
async parse(sql, options) {
|
|
188
|
+
this.assertRunning();
|
|
189
|
+
return this.httpPost('/parse', {
|
|
190
|
+
sql,
|
|
191
|
+
dialect: options.dialect,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* List all supported SQL dialects.
|
|
196
|
+
*/
|
|
197
|
+
async getDialects() {
|
|
198
|
+
this.assertRunning();
|
|
199
|
+
const result = await this.httpGet('/dialects');
|
|
200
|
+
return result.dialects;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check server health and return status information.
|
|
204
|
+
*/
|
|
205
|
+
async health() {
|
|
206
|
+
this.assertRunning();
|
|
207
|
+
const result = await this.httpGet('/health');
|
|
208
|
+
return {
|
|
209
|
+
status: result.status,
|
|
210
|
+
sqlglotVersion: result.sqlglot_version,
|
|
211
|
+
service: result.service,
|
|
212
|
+
port: this.port,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
assertRunning() {
|
|
216
|
+
if (!this.IsRunning) {
|
|
217
|
+
throw new Error('SqlGlotClient is not running. Call start() first.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Poll the health endpoint until the server is accepting requests.
|
|
222
|
+
* Uses short intervals with an overall timeout from startupTimeoutMs.
|
|
223
|
+
*/
|
|
224
|
+
async waitForReady() {
|
|
225
|
+
const deadline = Date.now() + this.startupTimeoutMs;
|
|
226
|
+
const interval = 50;
|
|
227
|
+
while (Date.now() < deadline) {
|
|
228
|
+
try {
|
|
229
|
+
await this.httpGet('/health');
|
|
230
|
+
return; // Server is ready
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`sqlglot-ts server did not become ready within ${this.startupTimeoutMs}ms`);
|
|
237
|
+
}
|
|
238
|
+
registerCleanup() {
|
|
239
|
+
if (this.cleanupRegistered)
|
|
240
|
+
return;
|
|
241
|
+
this.cleanupRegistered = true;
|
|
242
|
+
const cleanup = () => {
|
|
243
|
+
if (this.process) {
|
|
244
|
+
this.process.kill('SIGTERM');
|
|
245
|
+
this.process = null;
|
|
246
|
+
this.port = null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
process.on('exit', cleanup);
|
|
250
|
+
process.on('SIGINT', () => {
|
|
251
|
+
cleanup();
|
|
252
|
+
process.exit(0);
|
|
253
|
+
});
|
|
254
|
+
process.on('SIGTERM', () => {
|
|
255
|
+
cleanup();
|
|
256
|
+
process.exit(0);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
httpPost(path, body) {
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const data = JSON.stringify(body);
|
|
262
|
+
const req = http.request({
|
|
263
|
+
hostname: '127.0.0.1',
|
|
264
|
+
port: this.port,
|
|
265
|
+
path,
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
'Content-Type': 'application/json',
|
|
269
|
+
'Content-Length': Buffer.byteLength(data),
|
|
270
|
+
},
|
|
271
|
+
timeout: this.requestTimeoutMs,
|
|
272
|
+
}, (res) => {
|
|
273
|
+
let responseBody = '';
|
|
274
|
+
res.on('data', (chunk) => {
|
|
275
|
+
responseBody += chunk.toString();
|
|
276
|
+
});
|
|
277
|
+
res.on('end', () => {
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(responseBody);
|
|
280
|
+
resolve(parsed);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
reject(new Error(`Failed to parse response: ${responseBody.slice(0, 200)}`));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
req.on('error', (err) => {
|
|
288
|
+
reject(new Error(`HTTP request to sqlglot server failed: ${err.message}`));
|
|
289
|
+
});
|
|
290
|
+
req.on('timeout', () => {
|
|
291
|
+
req.destroy();
|
|
292
|
+
reject(new Error(`Request to ${path} timed out after ${this.requestTimeoutMs}ms`));
|
|
293
|
+
});
|
|
294
|
+
req.write(data);
|
|
295
|
+
req.end();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
httpGet(path) {
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const req = http.request({
|
|
301
|
+
hostname: '127.0.0.1',
|
|
302
|
+
port: this.port,
|
|
303
|
+
path,
|
|
304
|
+
method: 'GET',
|
|
305
|
+
timeout: this.requestTimeoutMs,
|
|
306
|
+
}, (res) => {
|
|
307
|
+
let responseBody = '';
|
|
308
|
+
res.on('data', (chunk) => {
|
|
309
|
+
responseBody += chunk.toString();
|
|
310
|
+
});
|
|
311
|
+
res.on('end', () => {
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(responseBody);
|
|
314
|
+
resolve(parsed);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
reject(new Error(`Failed to parse response: ${responseBody.slice(0, 200)}`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
req.on('error', (err) => {
|
|
322
|
+
reject(new Error(`HTTP request to sqlglot server failed: ${err.message}`));
|
|
323
|
+
});
|
|
324
|
+
req.on('timeout', () => {
|
|
325
|
+
req.destroy();
|
|
326
|
+
reject(new Error(`Request to ${path} timed out after ${this.requestTimeoutMs}ms`));
|
|
327
|
+
});
|
|
328
|
+
req.end();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=SqlGlotClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SqlGlotClient.js","sourceRoot":"","sources":["../src/SqlGlotClient.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,oBAAoB,EACpB,YAAY,GACb,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Supported SQL dialects (subset of sqlglot's 31 dialects) */
|
|
2
|
+
export type SQLDialect = 'tsql' | 'postgres' | 'mysql' | 'sqlite' | 'bigquery' | 'snowflake' | 'redshift' | 'spark' | 'duckdb' | 'oracle' | 'hive' | 'trino' | 'clickhouse' | 'databricks' | string;
|
|
3
|
+
export type ErrorLevel = 'IGNORE' | 'WARN' | 'RAISE' | 'IMMEDIATE';
|
|
4
|
+
export interface TranspileOptions {
|
|
5
|
+
/** Source SQL dialect */
|
|
6
|
+
fromDialect: SQLDialect;
|
|
7
|
+
/** Target SQL dialect */
|
|
8
|
+
toDialect: SQLDialect;
|
|
9
|
+
/** Pretty-print output (default: true) */
|
|
10
|
+
pretty?: boolean;
|
|
11
|
+
/** Error handling level (default: 'WARN') */
|
|
12
|
+
errorLevel?: ErrorLevel;
|
|
13
|
+
}
|
|
14
|
+
export interface TranspileResult {
|
|
15
|
+
/** Whether transpilation succeeded without errors */
|
|
16
|
+
success: boolean;
|
|
17
|
+
/** Combined SQL output (all statements joined with ;\n) */
|
|
18
|
+
sql: string;
|
|
19
|
+
/** Individual transpiled statements */
|
|
20
|
+
statements: string[];
|
|
21
|
+
/** Error messages */
|
|
22
|
+
errors: string[];
|
|
23
|
+
/** Warning messages */
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface ParseOptions {
|
|
27
|
+
/** SQL dialect to parse as */
|
|
28
|
+
dialect: SQLDialect;
|
|
29
|
+
}
|
|
30
|
+
export interface ParseResult {
|
|
31
|
+
/** Whether parsing succeeded */
|
|
32
|
+
success: boolean;
|
|
33
|
+
/** AST as JSON string */
|
|
34
|
+
ast: string;
|
|
35
|
+
/** Error messages */
|
|
36
|
+
errors: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface SqlGlotClientOptions {
|
|
39
|
+
/** Path to Python executable (default: 'python3') */
|
|
40
|
+
pythonPath?: string;
|
|
41
|
+
/** Path to the server.py file (default: auto-detected from package) */
|
|
42
|
+
serverPath?: string;
|
|
43
|
+
/** Startup timeout in ms (default: 30000) */
|
|
44
|
+
startupTimeoutMs?: number;
|
|
45
|
+
/** Request timeout in ms (default: 60000) */
|
|
46
|
+
requestTimeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface HealthStatus {
|
|
49
|
+
status: string;
|
|
50
|
+
sqlglotVersion: string;
|
|
51
|
+
service: string;
|
|
52
|
+
port: number;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,UAAU,GACV,OAAO,GACP,QAAQ,GACR,UAAU,GACV,WAAW,GACX,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,OAAO,GACP,YAAY,GACZ,YAAY,GACZ,MAAM,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;AAEnE,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,WAAW,EAAE,UAAU,CAAC;IACxB,yBAAyB;IACzB,SAAS,EAAE,UAAU,CAAC;IACtB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;IACjB,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/sqlglot-ts",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.5.0",
|
|
4
|
+
"description": "TypeScript wrapper for Python's sqlglot SQL transpiler. Manages a local Python FastAPI microservice to provide deterministic SQL dialect conversion.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist",
|
|
10
|
+
"/src/python"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && tsc-alias -f",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
16
|
+
},
|
|
5
17
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
18
|
+
"sql",
|
|
19
|
+
"transpiler",
|
|
20
|
+
"sqlglot",
|
|
21
|
+
"tsql",
|
|
22
|
+
"postgresql",
|
|
23
|
+
"mysql",
|
|
24
|
+
"dialect",
|
|
25
|
+
"conversion"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/MemberJunction/MJ"
|
|
30
|
+
},
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
}
|
|
10
37
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sqlglot-ts Python microservice.
|
|
3
|
+
Thin FastAPI wrapper around Python's sqlglot library (https://github.com/tobymao/sqlglot).
|
|
4
|
+
Runs on 127.0.0.1 with an ephemeral port for security.
|
|
5
|
+
"""
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
import sqlglot
|
|
9
|
+
import sqlglot.errors
|
|
10
|
+
import uvicorn
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
app = FastAPI(title="sqlglot-ts", version="1.0.0")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TranspileRequest(BaseModel):
|
|
18
|
+
sql: str
|
|
19
|
+
from_dialect: str
|
|
20
|
+
to_dialect: str
|
|
21
|
+
pretty: bool = True
|
|
22
|
+
error_level: str = "WARN" # IGNORE, WARN, RAISE, IMMEDIATE
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TranspileResponse(BaseModel):
|
|
26
|
+
success: bool
|
|
27
|
+
sql: str = ""
|
|
28
|
+
statements: list[str] = []
|
|
29
|
+
errors: list[str] = []
|
|
30
|
+
warnings: list[str] = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ParseRequest(BaseModel):
|
|
34
|
+
sql: str
|
|
35
|
+
dialect: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ParseResponse(BaseModel):
|
|
39
|
+
success: bool
|
|
40
|
+
ast: str = ""
|
|
41
|
+
errors: list[str] = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DialectsResponse(BaseModel):
|
|
45
|
+
dialects: list[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.post("/transpile", response_model=TranspileResponse)
|
|
49
|
+
def transpile(req: TranspileRequest):
|
|
50
|
+
"""Transpile SQL from one dialect to another."""
|
|
51
|
+
try:
|
|
52
|
+
error_level = getattr(
|
|
53
|
+
sqlglot.errors.ErrorLevel,
|
|
54
|
+
req.error_level,
|
|
55
|
+
sqlglot.errors.ErrorLevel.WARN,
|
|
56
|
+
)
|
|
57
|
+
results = sqlglot.transpile(
|
|
58
|
+
req.sql,
|
|
59
|
+
read=req.from_dialect,
|
|
60
|
+
write=req.to_dialect,
|
|
61
|
+
pretty=req.pretty,
|
|
62
|
+
error_level=error_level,
|
|
63
|
+
)
|
|
64
|
+
return TranspileResponse(
|
|
65
|
+
success=True,
|
|
66
|
+
sql=";\n".join(results) + (";" if results else ""),
|
|
67
|
+
statements=results,
|
|
68
|
+
errors=[],
|
|
69
|
+
warnings=[],
|
|
70
|
+
)
|
|
71
|
+
except sqlglot.errors.SqlglotError as e:
|
|
72
|
+
return TranspileResponse(
|
|
73
|
+
success=False,
|
|
74
|
+
sql="",
|
|
75
|
+
statements=[],
|
|
76
|
+
errors=[str(e)],
|
|
77
|
+
warnings=[],
|
|
78
|
+
)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return TranspileResponse(
|
|
81
|
+
success=False,
|
|
82
|
+
sql="",
|
|
83
|
+
statements=[],
|
|
84
|
+
errors=[f"Unexpected error: {str(e)}"],
|
|
85
|
+
warnings=[],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.post("/transpile-statements", response_model=TranspileResponse)
|
|
90
|
+
def transpile_statements(req: TranspileRequest):
|
|
91
|
+
"""
|
|
92
|
+
Transpile SQL statement-by-statement.
|
|
93
|
+
Returns individual results for each statement, useful for
|
|
94
|
+
identifying which specific statements fail conversion.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
error_level = getattr(
|
|
98
|
+
sqlglot.errors.ErrorLevel,
|
|
99
|
+
req.error_level,
|
|
100
|
+
sqlglot.errors.ErrorLevel.WARN,
|
|
101
|
+
)
|
|
102
|
+
# Parse into individual statements first
|
|
103
|
+
parsed = sqlglot.parse(
|
|
104
|
+
req.sql, read=req.from_dialect, error_level=error_level
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
results: list[str] = []
|
|
108
|
+
errors: list[str] = []
|
|
109
|
+
warnings: list[str] = []
|
|
110
|
+
|
|
111
|
+
for i, stmt in enumerate(parsed):
|
|
112
|
+
if stmt is None:
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
transpiled = stmt.sql(dialect=req.to_dialect, pretty=req.pretty)
|
|
116
|
+
results.append(transpiled)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
original = stmt.sql(dialect=req.from_dialect)
|
|
119
|
+
errors.append(
|
|
120
|
+
f"Statement {i+1} failed: {str(e)} | Original: {original[:200]}"
|
|
121
|
+
)
|
|
122
|
+
results.append(f"-- FAILED: {original}")
|
|
123
|
+
|
|
124
|
+
return TranspileResponse(
|
|
125
|
+
success=len(errors) == 0,
|
|
126
|
+
sql=";\n".join(results) + (";" if results else ""),
|
|
127
|
+
statements=results,
|
|
128
|
+
errors=errors,
|
|
129
|
+
warnings=warnings,
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return TranspileResponse(
|
|
133
|
+
success=False,
|
|
134
|
+
sql="",
|
|
135
|
+
statements=[],
|
|
136
|
+
errors=[str(e)],
|
|
137
|
+
warnings=[],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.post("/parse", response_model=ParseResponse)
|
|
142
|
+
def parse(req: ParseRequest):
|
|
143
|
+
"""Parse SQL and return the AST as JSON."""
|
|
144
|
+
try:
|
|
145
|
+
parsed = sqlglot.parse(req.sql, read=req.dialect)
|
|
146
|
+
ast_json = [stmt.dump() if stmt else None for stmt in parsed]
|
|
147
|
+
return ParseResponse(success=True, ast=json.dumps(ast_json, indent=2))
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return ParseResponse(success=False, errors=[str(e)])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.get("/dialects", response_model=DialectsResponse)
|
|
153
|
+
def dialects():
|
|
154
|
+
"""List all supported SQL dialects."""
|
|
155
|
+
actual = sorted(sqlglot.Dialect.classes.keys())
|
|
156
|
+
return DialectsResponse(dialects=actual)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.get("/health")
|
|
160
|
+
def health():
|
|
161
|
+
"""Health check endpoint."""
|
|
162
|
+
return {
|
|
163
|
+
"status": "ok",
|
|
164
|
+
"sqlglot_version": sqlglot.__version__,
|
|
165
|
+
"service": "sqlglot-ts",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
import socket
|
|
171
|
+
|
|
172
|
+
requested_port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
|
173
|
+
|
|
174
|
+
# Bind a socket to discover the ephemeral port, then close it and
|
|
175
|
+
# pass that port to uvicorn. There is a tiny race window but it is
|
|
176
|
+
# acceptable for a local-only dev tool.
|
|
177
|
+
if requested_port == 0:
|
|
178
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
179
|
+
sock.bind(("127.0.0.1", 0))
|
|
180
|
+
actual_port = sock.getsockname()[1]
|
|
181
|
+
sock.close()
|
|
182
|
+
else:
|
|
183
|
+
actual_port = requested_port
|
|
184
|
+
|
|
185
|
+
# Print the port BEFORE uvicorn starts so the TS client can read it.
|
|
186
|
+
print(f"SQLGLOT_PORT={actual_port}", flush=True)
|
|
187
|
+
|
|
188
|
+
uvicorn.run(app, host="127.0.0.1", port=actual_port, log_level="warning")
|