@bytebase/dbhub 0.16.1 → 0.18.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.
|
@@ -296,7 +296,9 @@ var SSHTunnel = class {
|
|
|
296
296
|
privateKey,
|
|
297
297
|
targetConfig.passphrase,
|
|
298
298
|
previousStream,
|
|
299
|
-
`jump host ${i + 1}
|
|
299
|
+
`jump host ${i + 1}`,
|
|
300
|
+
targetConfig.keepaliveInterval,
|
|
301
|
+
targetConfig.keepaliveCountMax
|
|
300
302
|
);
|
|
301
303
|
console.error(` \u2192 Forwarding through ${jumpHost.host}:${jumpHost.port} to ${nextHost.host}:${nextHost.port}`);
|
|
302
304
|
forwardStream = await this.forwardTo(client, nextHost.host, nextHost.port);
|
|
@@ -322,7 +324,9 @@ var SSHTunnel = class {
|
|
|
322
324
|
privateKey,
|
|
323
325
|
targetConfig.passphrase,
|
|
324
326
|
previousStream,
|
|
325
|
-
jumpHosts.length > 0 ? "target host" : void 0
|
|
327
|
+
jumpHosts.length > 0 ? "target host" : void 0,
|
|
328
|
+
targetConfig.keepaliveInterval,
|
|
329
|
+
targetConfig.keepaliveCountMax
|
|
326
330
|
);
|
|
327
331
|
this.sshClients.push(finalClient);
|
|
328
332
|
return finalClient;
|
|
@@ -330,7 +334,7 @@ var SSHTunnel = class {
|
|
|
330
334
|
/**
|
|
331
335
|
* Connect to a single SSH host.
|
|
332
336
|
*/
|
|
333
|
-
connectToHost(hostInfo, password, privateKey, passphrase, sock, label) {
|
|
337
|
+
connectToHost(hostInfo, password, privateKey, passphrase, sock, label, keepaliveInterval, keepaliveCountMax) {
|
|
334
338
|
return new Promise((resolve, reject) => {
|
|
335
339
|
const client = new Client();
|
|
336
340
|
const sshConfig = {
|
|
@@ -350,6 +354,17 @@ var SSHTunnel = class {
|
|
|
350
354
|
if (sock) {
|
|
351
355
|
sshConfig.sock = sock;
|
|
352
356
|
}
|
|
357
|
+
if (keepaliveInterval !== void 0) {
|
|
358
|
+
if (Number.isNaN(keepaliveInterval) || keepaliveInterval < 0) {
|
|
359
|
+
const desc = label || `${hostInfo.host}:${hostInfo.port}`;
|
|
360
|
+
console.warn(
|
|
361
|
+
`Invalid SSH keepaliveInterval (${keepaliveInterval}) for ${desc}; keepalive configuration will be ignored.`
|
|
362
|
+
);
|
|
363
|
+
} else if (keepaliveInterval > 0) {
|
|
364
|
+
sshConfig.keepaliveInterval = keepaliveInterval * 1e3;
|
|
365
|
+
sshConfig.keepaliveCountMax = keepaliveCountMax ?? 3;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
353
368
|
const onError = (err) => {
|
|
354
369
|
client.removeListener("ready", onReady);
|
|
355
370
|
client.destroy();
|
|
@@ -1011,6 +1026,27 @@ function resolveSSHConfig() {
|
|
|
1011
1026
|
config.proxyJump = process.env.SSH_PROXY_JUMP;
|
|
1012
1027
|
sources.push("SSH_PROXY_JUMP from environment");
|
|
1013
1028
|
}
|
|
1029
|
+
const parseNonNegativeInteger = (value, name) => {
|
|
1030
|
+
const parsed = Number(value);
|
|
1031
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
1032
|
+
throw new Error(`Invalid value for ${name}: "${value}". Expected a non-negative integer.`);
|
|
1033
|
+
}
|
|
1034
|
+
return parsed;
|
|
1035
|
+
};
|
|
1036
|
+
if (args["ssh-keepalive-interval"]) {
|
|
1037
|
+
config.keepaliveInterval = parseNonNegativeInteger(args["ssh-keepalive-interval"], "ssh-keepalive-interval");
|
|
1038
|
+
sources.push("ssh-keepalive-interval from command line");
|
|
1039
|
+
} else if (process.env.SSH_KEEPALIVE_INTERVAL) {
|
|
1040
|
+
config.keepaliveInterval = parseNonNegativeInteger(process.env.SSH_KEEPALIVE_INTERVAL, "SSH_KEEPALIVE_INTERVAL");
|
|
1041
|
+
sources.push("SSH_KEEPALIVE_INTERVAL from environment");
|
|
1042
|
+
}
|
|
1043
|
+
if (args["ssh-keepalive-count-max"]) {
|
|
1044
|
+
config.keepaliveCountMax = parseNonNegativeInteger(args["ssh-keepalive-count-max"], "ssh-keepalive-count-max");
|
|
1045
|
+
sources.push("ssh-keepalive-count-max from command line");
|
|
1046
|
+
} else if (process.env.SSH_KEEPALIVE_COUNT_MAX) {
|
|
1047
|
+
config.keepaliveCountMax = parseNonNegativeInteger(process.env.SSH_KEEPALIVE_COUNT_MAX, "SSH_KEEPALIVE_COUNT_MAX");
|
|
1048
|
+
sources.push("SSH_KEEPALIVE_COUNT_MAX from environment");
|
|
1049
|
+
}
|
|
1014
1050
|
if (!config.host || !config.username) {
|
|
1015
1051
|
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
1016
1052
|
}
|
|
@@ -1090,6 +1126,8 @@ async function resolveSourceConfigs() {
|
|
|
1090
1126
|
source.ssh_password = sshResult.config.password;
|
|
1091
1127
|
source.ssh_key = sshResult.config.privateKey;
|
|
1092
1128
|
source.ssh_passphrase = sshResult.config.passphrase;
|
|
1129
|
+
source.ssh_keepalive_interval = sshResult.config.keepaliveInterval;
|
|
1130
|
+
source.ssh_keepalive_count_max = sshResult.config.keepaliveCountMax;
|
|
1093
1131
|
}
|
|
1094
1132
|
if (dsnResult.isDemo) {
|
|
1095
1133
|
const { getSqliteInMemorySetupSql } = await import("./demo-loader-PSMTLZ2T.js");
|
|
@@ -1270,6 +1308,31 @@ function validateSourceConfig(source, configPath) {
|
|
|
1270
1308
|
);
|
|
1271
1309
|
}
|
|
1272
1310
|
}
|
|
1311
|
+
if (source.aws_iam_auth !== void 0 && typeof source.aws_iam_auth !== "boolean") {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid aws_iam_auth. Must be a boolean (true or false).`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (source.aws_region !== void 0) {
|
|
1317
|
+
if (typeof source.aws_region !== "string" || source.aws_region.trim().length === 0) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid aws_region. Must be a non-empty string (e.g., "eu-west-1").`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (source.aws_iam_auth === true) {
|
|
1324
|
+
const validIamTypes = ["postgres", "mysql", "mariadb"];
|
|
1325
|
+
if (!source.type || !validIamTypes.includes(source.type)) {
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
`Configuration file ${configPath}: source '${source.id}' has aws_iam_auth enabled, but this is only supported for postgres, mysql, and mariadb sources.`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (!source.aws_region) {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
`Configuration file ${configPath}: source '${source.id}' has aws_iam_auth enabled but aws_region is not specified.`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1273
1336
|
if (source.connection_timeout !== void 0) {
|
|
1274
1337
|
if (typeof source.connection_timeout !== "number" || source.connection_timeout <= 0) {
|
|
1275
1338
|
throw new Error(
|
|
@@ -1339,6 +1402,18 @@ function validateSourceConfig(source, configPath) {
|
|
|
1339
1402
|
);
|
|
1340
1403
|
}
|
|
1341
1404
|
}
|
|
1405
|
+
if (source.search_path !== void 0) {
|
|
1406
|
+
if (source.type !== "postgres") {
|
|
1407
|
+
throw new Error(
|
|
1408
|
+
`Configuration file ${configPath}: source '${source.id}' has 'search_path' but it is only supported for PostgreSQL sources.`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (typeof source.search_path !== "string" || source.search_path.trim().length === 0) {
|
|
1412
|
+
throw new Error(
|
|
1413
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid search_path. Must be a non-empty string of comma-separated schema names (e.g., "myschema,public").`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1342
1417
|
if (source.readonly !== void 0) {
|
|
1343
1418
|
throw new Error(
|
|
1344
1419
|
`Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
|
|
@@ -1408,7 +1483,8 @@ function buildDSNFromSource(source) {
|
|
|
1408
1483
|
}
|
|
1409
1484
|
return `sqlite:///${source.database}`;
|
|
1410
1485
|
}
|
|
1411
|
-
const
|
|
1486
|
+
const isAwsIamPasswordless = source.aws_iam_auth === true && ["postgres", "mysql", "mariadb"].includes(source.type);
|
|
1487
|
+
const passwordRequired = source.authentication !== "azure-active-directory-access-token" && !isAwsIamPasswordless;
|
|
1412
1488
|
if (!source.host || !source.user || !source.database) {
|
|
1413
1489
|
throw new Error(
|
|
1414
1490
|
`Source '${source.id}': missing required connection parameters. Required: type, host, user, database`
|
|
@@ -1416,7 +1492,7 @@ function buildDSNFromSource(source) {
|
|
|
1416
1492
|
}
|
|
1417
1493
|
if (passwordRequired && !source.password) {
|
|
1418
1494
|
throw new Error(
|
|
1419
|
-
`Source '${source.id}': password is required. (Password is optional
|
|
1495
|
+
`Source '${source.id}': password is required. (Password is optional for azure-active-directory-access-token authentication or when aws_iam_auth=true)`
|
|
1420
1496
|
);
|
|
1421
1497
|
}
|
|
1422
1498
|
const port = source.port || getDefaultPortForType(source.type);
|
|
@@ -1448,8 +1524,21 @@ function buildDSNFromSource(source) {
|
|
|
1448
1524
|
return dsn;
|
|
1449
1525
|
}
|
|
1450
1526
|
|
|
1527
|
+
// src/utils/aws-rds-signer.ts
|
|
1528
|
+
import { Signer } from "@aws-sdk/rds-signer";
|
|
1529
|
+
async function generateRdsAuthToken(params) {
|
|
1530
|
+
const signer = new Signer({
|
|
1531
|
+
hostname: params.hostname,
|
|
1532
|
+
port: params.port,
|
|
1533
|
+
username: params.username,
|
|
1534
|
+
region: params.region
|
|
1535
|
+
});
|
|
1536
|
+
return signer.getAuthToken();
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1451
1539
|
// src/connectors/manager.ts
|
|
1452
1540
|
var managerInstance = null;
|
|
1541
|
+
var AWS_IAM_TOKEN_REFRESH_MS = 14 * 60 * 1e3;
|
|
1453
1542
|
var ConnectorManager = class {
|
|
1454
1543
|
// Prevent race conditions
|
|
1455
1544
|
constructor() {
|
|
@@ -1460,6 +1549,8 @@ var ConnectorManager = class {
|
|
|
1460
1549
|
// Store original source configs
|
|
1461
1550
|
this.sourceIds = [];
|
|
1462
1551
|
// Ordered list of source IDs (first is default)
|
|
1552
|
+
this.iamRefreshTimers = /* @__PURE__ */ new Map();
|
|
1553
|
+
this.isDisconnecting = false;
|
|
1463
1554
|
// Lazy connection support
|
|
1464
1555
|
this.lazySources = /* @__PURE__ */ new Map();
|
|
1465
1556
|
// Sources pending lazy connection
|
|
@@ -1549,7 +1640,7 @@ var ConnectorManager = class {
|
|
|
1549
1640
|
*/
|
|
1550
1641
|
async connectSource(source) {
|
|
1551
1642
|
const sourceId = source.id;
|
|
1552
|
-
const dsn =
|
|
1643
|
+
const dsn = await this.buildConnectionDSN(source);
|
|
1553
1644
|
console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
|
|
1554
1645
|
let actualDSN = dsn;
|
|
1555
1646
|
if (source.ssh_host) {
|
|
@@ -1565,7 +1656,9 @@ var ConnectorManager = class {
|
|
|
1565
1656
|
password: source.ssh_password,
|
|
1566
1657
|
privateKey: source.ssh_key,
|
|
1567
1658
|
passphrase: source.ssh_passphrase,
|
|
1568
|
-
proxyJump: source.ssh_proxy_jump
|
|
1659
|
+
proxyJump: source.ssh_proxy_jump,
|
|
1660
|
+
keepaliveInterval: source.ssh_keepalive_interval,
|
|
1661
|
+
keepaliveCountMax: source.ssh_keepalive_count_max
|
|
1569
1662
|
};
|
|
1570
1663
|
if (!sshConfig.password && !sshConfig.privateKey) {
|
|
1571
1664
|
throw new Error(
|
|
@@ -1606,17 +1699,26 @@ var ConnectorManager = class {
|
|
|
1606
1699
|
if (source.readonly !== void 0) {
|
|
1607
1700
|
config.readonly = source.readonly;
|
|
1608
1701
|
}
|
|
1702
|
+
if (source.search_path) {
|
|
1703
|
+
config.searchPath = source.search_path;
|
|
1704
|
+
}
|
|
1609
1705
|
await connector.connect(actualDSN, source.init_script, config);
|
|
1610
1706
|
this.connectors.set(sourceId, connector);
|
|
1611
1707
|
if (!this.sourceIds.includes(sourceId)) {
|
|
1612
1708
|
this.sourceIds.push(sourceId);
|
|
1613
1709
|
}
|
|
1614
1710
|
this.sourceConfigs.set(sourceId, source);
|
|
1711
|
+
this.scheduleIamRefresh(source);
|
|
1615
1712
|
}
|
|
1616
1713
|
/**
|
|
1617
1714
|
* Close all database connections
|
|
1618
1715
|
*/
|
|
1619
1716
|
async disconnect() {
|
|
1717
|
+
this.isDisconnecting = true;
|
|
1718
|
+
for (const timer of this.iamRefreshTimers.values()) {
|
|
1719
|
+
clearTimeout(timer);
|
|
1720
|
+
}
|
|
1721
|
+
this.iamRefreshTimers.clear();
|
|
1620
1722
|
for (const [sourceId, connector] of this.connectors.entries()) {
|
|
1621
1723
|
try {
|
|
1622
1724
|
await connector.disconnect();
|
|
@@ -1638,6 +1740,7 @@ var ConnectorManager = class {
|
|
|
1638
1740
|
this.lazySources.clear();
|
|
1639
1741
|
this.pendingConnections.clear();
|
|
1640
1742
|
this.sourceIds = [];
|
|
1743
|
+
this.isDisconnecting = false;
|
|
1641
1744
|
}
|
|
1642
1745
|
/**
|
|
1643
1746
|
* Get a connector by source ID
|
|
@@ -1738,63 +1841,317 @@ var ConnectorManager = class {
|
|
|
1738
1841
|
}
|
|
1739
1842
|
return getDefaultPortForType(type) ?? 0;
|
|
1740
1843
|
}
|
|
1844
|
+
scheduleIamRefresh(source) {
|
|
1845
|
+
if (this.isDisconnecting) {
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const sourceId = source.id;
|
|
1849
|
+
const existingTimer = this.iamRefreshTimers.get(sourceId);
|
|
1850
|
+
if (existingTimer) {
|
|
1851
|
+
clearTimeout(existingTimer);
|
|
1852
|
+
this.iamRefreshTimers.delete(sourceId);
|
|
1853
|
+
}
|
|
1854
|
+
if (!source.aws_iam_auth) {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
const timer = setTimeout(async () => {
|
|
1858
|
+
if (this.isDisconnecting) {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
await this.refreshIamSourceConnection(source);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
console.error(
|
|
1865
|
+
`Error refreshing AWS IAM auth token for source '${sourceId}':`,
|
|
1866
|
+
error
|
|
1867
|
+
);
|
|
1868
|
+
} finally {
|
|
1869
|
+
if (!this.isDisconnecting && this.sourceConfigs.has(sourceId)) {
|
|
1870
|
+
this.scheduleIamRefresh(source);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}, AWS_IAM_TOKEN_REFRESH_MS);
|
|
1874
|
+
timer.unref?.();
|
|
1875
|
+
this.iamRefreshTimers.set(sourceId, timer);
|
|
1876
|
+
}
|
|
1877
|
+
async refreshIamSourceConnection(source) {
|
|
1878
|
+
const sourceId = source.id;
|
|
1879
|
+
if (this.isDisconnecting || !source.aws_iam_auth || !this.connectors.has(sourceId)) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
console.error(`Refreshing AWS IAM auth connection for source '${sourceId}'...`);
|
|
1883
|
+
const existingConnector = this.connectors.get(sourceId);
|
|
1884
|
+
if (existingConnector) {
|
|
1885
|
+
await existingConnector.disconnect();
|
|
1886
|
+
this.connectors.delete(sourceId);
|
|
1887
|
+
}
|
|
1888
|
+
const existingTunnel = this.sshTunnels.get(sourceId);
|
|
1889
|
+
if (existingTunnel) {
|
|
1890
|
+
await existingTunnel.close();
|
|
1891
|
+
this.sshTunnels.delete(sourceId);
|
|
1892
|
+
}
|
|
1893
|
+
if (this.isDisconnecting) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
await this.connectSource(source);
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Build a connection DSN, optionally replacing password with
|
|
1900
|
+
* an AWS RDS IAM auth token when aws_iam_auth is enabled.
|
|
1901
|
+
*/
|
|
1902
|
+
async buildConnectionDSN(source) {
|
|
1903
|
+
const dsn = buildDSNFromSource(source);
|
|
1904
|
+
if (!source.aws_iam_auth) {
|
|
1905
|
+
return dsn;
|
|
1906
|
+
}
|
|
1907
|
+
const supportedIamTypes = ["postgres", "mysql", "mariadb"];
|
|
1908
|
+
if (!source.type || !supportedIamTypes.includes(source.type)) {
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
`Source '${source.id}': aws_iam_auth is only supported for postgres, mysql, and mariadb`
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
if (!source.aws_region) {
|
|
1914
|
+
throw new Error(
|
|
1915
|
+
`Source '${source.id}': aws_region is required when aws_iam_auth is enabled`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
const parsed = new SafeURL(dsn);
|
|
1919
|
+
const hostname = parsed.hostname;
|
|
1920
|
+
const username = source.user || parsed.username;
|
|
1921
|
+
const defaultPort = getDefaultPortForType(source.type);
|
|
1922
|
+
const port = parsed.port ? parseInt(parsed.port) : defaultPort;
|
|
1923
|
+
if (!hostname || !username || !port) {
|
|
1924
|
+
throw new Error(
|
|
1925
|
+
`Source '${source.id}': unable to resolve host, username, or port for AWS IAM authentication`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
const token = await generateRdsAuthToken({
|
|
1929
|
+
hostname,
|
|
1930
|
+
port,
|
|
1931
|
+
username,
|
|
1932
|
+
region: source.aws_region
|
|
1933
|
+
});
|
|
1934
|
+
const queryParams = new Map(parsed.searchParams);
|
|
1935
|
+
queryParams.set("sslmode", "require");
|
|
1936
|
+
const protocol = parsed.protocol.endsWith(":") ? parsed.protocol.slice(0, -1) : parsed.protocol;
|
|
1937
|
+
const encodedUser = encodeURIComponent(username);
|
|
1938
|
+
const encodedToken = encodeURIComponent(token);
|
|
1939
|
+
const path3 = parsed.pathname || "/";
|
|
1940
|
+
const query = Array.from(queryParams.entries()).map(
|
|
1941
|
+
([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
1942
|
+
).join("&");
|
|
1943
|
+
return `${protocol}://${encodedUser}:${encodedToken}@${hostname}:${port}${path3}${query ? `?${query}` : ""}`;
|
|
1944
|
+
}
|
|
1741
1945
|
};
|
|
1742
1946
|
|
|
1743
1947
|
// src/utils/sql-parser.ts
|
|
1744
|
-
|
|
1745
|
-
|
|
1948
|
+
var TokenType = { Plain: 0, Comment: 1, QuotedBlock: 2 };
|
|
1949
|
+
function plainToken(i) {
|
|
1950
|
+
return { type: TokenType.Plain, end: i + 1 };
|
|
1951
|
+
}
|
|
1952
|
+
function scanSingleLineComment(sql, i) {
|
|
1953
|
+
if (sql[i] !== "-" || sql[i + 1] !== "-") {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
let j = i;
|
|
1957
|
+
while (j < sql.length && sql[j] !== "\n") {
|
|
1958
|
+
j++;
|
|
1959
|
+
}
|
|
1960
|
+
return { type: TokenType.Comment, end: j };
|
|
1961
|
+
}
|
|
1962
|
+
function scanMultiLineComment(sql, i) {
|
|
1963
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
let j = i + 2;
|
|
1967
|
+
while (j < sql.length && !(sql[j] === "*" && sql[j + 1] === "/")) {
|
|
1968
|
+
j++;
|
|
1969
|
+
}
|
|
1970
|
+
if (j < sql.length) {
|
|
1971
|
+
j += 2;
|
|
1972
|
+
}
|
|
1973
|
+
return { type: TokenType.Comment, end: j };
|
|
1974
|
+
}
|
|
1975
|
+
function scanNestedMultiLineComment(sql, i) {
|
|
1976
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
let j = i + 2;
|
|
1980
|
+
let depth = 1;
|
|
1981
|
+
while (j < sql.length && depth > 0) {
|
|
1982
|
+
if (sql[j] === "/" && sql[j + 1] === "*") {
|
|
1983
|
+
depth++;
|
|
1984
|
+
j += 2;
|
|
1985
|
+
} else if (sql[j] === "*" && sql[j + 1] === "/") {
|
|
1986
|
+
depth--;
|
|
1987
|
+
j += 2;
|
|
1988
|
+
} else {
|
|
1989
|
+
j++;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return { type: TokenType.Comment, end: j };
|
|
1993
|
+
}
|
|
1994
|
+
function scanSingleQuotedString(sql, i) {
|
|
1995
|
+
if (sql[i] !== "'") {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
let j = i + 1;
|
|
1999
|
+
while (j < sql.length) {
|
|
2000
|
+
if (sql[j] === "'" && sql[j + 1] === "'") {
|
|
2001
|
+
j += 2;
|
|
2002
|
+
} else if (sql[j] === "'") {
|
|
2003
|
+
j++;
|
|
2004
|
+
break;
|
|
2005
|
+
} else {
|
|
2006
|
+
j++;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
2010
|
+
}
|
|
2011
|
+
function scanDoubleQuotedString(sql, i) {
|
|
2012
|
+
if (sql[i] !== '"') {
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
let j = i + 1;
|
|
2016
|
+
while (j < sql.length) {
|
|
2017
|
+
if (sql[j] === '"' && sql[j + 1] === '"') {
|
|
2018
|
+
j += 2;
|
|
2019
|
+
} else if (sql[j] === '"') {
|
|
2020
|
+
j++;
|
|
2021
|
+
break;
|
|
2022
|
+
} else {
|
|
2023
|
+
j++;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
2027
|
+
}
|
|
2028
|
+
var dollarQuoteOpenRegex = /^\$([a-zA-Z_]\w*)?\$/;
|
|
2029
|
+
function scanDollarQuotedBlock(sql, i) {
|
|
2030
|
+
if (sql[i] !== "$") {
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
const next = sql[i + 1];
|
|
2034
|
+
if (next >= "0" && next <= "9") {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
const remaining = sql.substring(i);
|
|
2038
|
+
const m = dollarQuoteOpenRegex.exec(remaining);
|
|
2039
|
+
if (!m) {
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
const tag = m[0];
|
|
2043
|
+
const bodyStart = i + tag.length;
|
|
2044
|
+
const closeIdx = sql.indexOf(tag, bodyStart);
|
|
2045
|
+
const end = closeIdx !== -1 ? closeIdx + tag.length : sql.length;
|
|
2046
|
+
return { type: TokenType.QuotedBlock, end };
|
|
2047
|
+
}
|
|
2048
|
+
function scanBacktickQuotedIdentifier(sql, i) {
|
|
2049
|
+
if (sql[i] !== "`") {
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
let j = i + 1;
|
|
2053
|
+
while (j < sql.length) {
|
|
2054
|
+
if (sql[j] === "`" && sql[j + 1] === "`") {
|
|
2055
|
+
j += 2;
|
|
2056
|
+
} else if (sql[j] === "`") {
|
|
2057
|
+
j++;
|
|
2058
|
+
break;
|
|
2059
|
+
} else {
|
|
2060
|
+
j++;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
2064
|
+
}
|
|
2065
|
+
function scanBracketQuotedIdentifier(sql, i) {
|
|
2066
|
+
if (sql[i] !== "[") {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
let j = i + 1;
|
|
2070
|
+
while (j < sql.length) {
|
|
2071
|
+
if (sql[j] === "]" && sql[j + 1] === "]") {
|
|
2072
|
+
j += 2;
|
|
2073
|
+
} else if (sql[j] === "]") {
|
|
2074
|
+
j++;
|
|
2075
|
+
break;
|
|
2076
|
+
} else {
|
|
2077
|
+
j++;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
2081
|
+
}
|
|
2082
|
+
function scanTokenAnsi(sql, i) {
|
|
2083
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
|
|
2084
|
+
}
|
|
2085
|
+
function scanTokenPostgres(sql, i) {
|
|
2086
|
+
return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
|
|
2087
|
+
}
|
|
2088
|
+
function scanTokenMySQL(sql, i) {
|
|
2089
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
2090
|
+
}
|
|
2091
|
+
function scanTokenSQLite(sql, i) {
|
|
2092
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
2093
|
+
}
|
|
2094
|
+
function scanTokenSQLServer(sql, i) {
|
|
2095
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
2096
|
+
}
|
|
2097
|
+
var dialectScanners = {
|
|
2098
|
+
postgres: scanTokenPostgres,
|
|
2099
|
+
mysql: scanTokenMySQL,
|
|
2100
|
+
mariadb: scanTokenMySQL,
|
|
2101
|
+
sqlite: scanTokenSQLite,
|
|
2102
|
+
sqlserver: scanTokenSQLServer
|
|
2103
|
+
};
|
|
2104
|
+
function getScanner(dialect) {
|
|
2105
|
+
return dialect ? dialectScanners[dialect] ?? scanTokenAnsi : scanTokenAnsi;
|
|
2106
|
+
}
|
|
2107
|
+
function stripCommentsAndStrings(sql, dialect) {
|
|
2108
|
+
const scanToken = getScanner(dialect);
|
|
2109
|
+
const parts = [];
|
|
2110
|
+
let plainStart = -1;
|
|
1746
2111
|
let i = 0;
|
|
1747
2112
|
while (i < sql.length) {
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2113
|
+
const token = scanToken(sql, i);
|
|
2114
|
+
if (token.type === TokenType.Plain) {
|
|
2115
|
+
if (plainStart === -1) {
|
|
2116
|
+
plainStart = i;
|
|
1751
2117
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
i += 2;
|
|
1757
|
-
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
|
|
1758
|
-
i++;
|
|
2118
|
+
} else {
|
|
2119
|
+
if (plainStart !== -1) {
|
|
2120
|
+
parts.push(sql.substring(plainStart, i));
|
|
2121
|
+
plainStart = -1;
|
|
1759
2122
|
}
|
|
1760
|
-
|
|
1761
|
-
result += " ";
|
|
1762
|
-
continue;
|
|
2123
|
+
parts.push(" ");
|
|
1763
2124
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2125
|
+
i = token.end;
|
|
2126
|
+
}
|
|
2127
|
+
if (plainStart !== -1) {
|
|
2128
|
+
parts.push(sql.substring(plainStart));
|
|
2129
|
+
}
|
|
2130
|
+
return parts.join("");
|
|
2131
|
+
}
|
|
2132
|
+
function splitSQLStatements(sql, dialect) {
|
|
2133
|
+
const scanToken = getScanner(dialect);
|
|
2134
|
+
const statements = [];
|
|
2135
|
+
let stmtStart = 0;
|
|
2136
|
+
let i = 0;
|
|
2137
|
+
while (i < sql.length) {
|
|
2138
|
+
if (sql[i] === ";") {
|
|
2139
|
+
const trimmed2 = sql.substring(stmtStart, i).trim();
|
|
2140
|
+
if (trimmed2.length > 0) {
|
|
2141
|
+
statements.push(trimmed2);
|
|
1775
2142
|
}
|
|
1776
|
-
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
if (sql[i] === '"') {
|
|
2143
|
+
stmtStart = i + 1;
|
|
1780
2144
|
i++;
|
|
1781
|
-
while (i < sql.length) {
|
|
1782
|
-
if (sql[i] === '"' && sql[i + 1] === '"') {
|
|
1783
|
-
i += 2;
|
|
1784
|
-
} else if (sql[i] === '"') {
|
|
1785
|
-
i++;
|
|
1786
|
-
break;
|
|
1787
|
-
} else {
|
|
1788
|
-
i++;
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
result += " ";
|
|
1792
2145
|
continue;
|
|
1793
2146
|
}
|
|
1794
|
-
|
|
1795
|
-
i
|
|
2147
|
+
const token = scanToken(sql, i);
|
|
2148
|
+
i = token.end;
|
|
2149
|
+
}
|
|
2150
|
+
const trimmed = sql.substring(stmtStart).trim();
|
|
2151
|
+
if (trimmed.length > 0) {
|
|
2152
|
+
statements.push(trimmed);
|
|
1796
2153
|
}
|
|
1797
|
-
return
|
|
2154
|
+
return statements;
|
|
1798
2155
|
}
|
|
1799
2156
|
|
|
1800
2157
|
// src/utils/parameter-mapper.ts
|
|
@@ -2130,12 +2487,15 @@ export {
|
|
|
2130
2487
|
getDatabaseTypeFromDSN,
|
|
2131
2488
|
getDefaultPortForType,
|
|
2132
2489
|
stripCommentsAndStrings,
|
|
2490
|
+
splitSQLStatements,
|
|
2133
2491
|
isDemoMode,
|
|
2134
2492
|
resolveTransport,
|
|
2135
2493
|
resolvePort,
|
|
2136
2494
|
resolveSourceConfigs,
|
|
2137
2495
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
2138
2496
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
2497
|
+
loadTomlConfig,
|
|
2498
|
+
resolveTomlConfigPath,
|
|
2139
2499
|
ConnectorManager,
|
|
2140
2500
|
mapArgumentsToArray,
|
|
2141
2501
|
ToolRegistry,
|