@teckedd-code2save/datafy 0.17.0 → 1.0.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.
|
@@ -302,7 +302,9 @@ var SSHTunnel = class {
|
|
|
302
302
|
privateKey,
|
|
303
303
|
targetConfig.passphrase,
|
|
304
304
|
previousStream,
|
|
305
|
-
`jump host ${i + 1}
|
|
305
|
+
`jump host ${i + 1}`,
|
|
306
|
+
targetConfig.keepaliveInterval,
|
|
307
|
+
targetConfig.keepaliveCountMax
|
|
306
308
|
);
|
|
307
309
|
console.error(` \u2192 Forwarding through ${jumpHost.host}:${jumpHost.port} to ${nextHost.host}:${nextHost.port}`);
|
|
308
310
|
forwardStream = await this.forwardTo(client, nextHost.host, nextHost.port);
|
|
@@ -328,7 +330,9 @@ var SSHTunnel = class {
|
|
|
328
330
|
privateKey,
|
|
329
331
|
targetConfig.passphrase,
|
|
330
332
|
previousStream,
|
|
331
|
-
jumpHosts.length > 0 ? "target host" : void 0
|
|
333
|
+
jumpHosts.length > 0 ? "target host" : void 0,
|
|
334
|
+
targetConfig.keepaliveInterval,
|
|
335
|
+
targetConfig.keepaliveCountMax
|
|
332
336
|
);
|
|
333
337
|
this.sshClients.push(finalClient);
|
|
334
338
|
return finalClient;
|
|
@@ -336,7 +340,7 @@ var SSHTunnel = class {
|
|
|
336
340
|
/**
|
|
337
341
|
* Connect to a single SSH host.
|
|
338
342
|
*/
|
|
339
|
-
connectToHost(hostInfo, password, privateKey, passphrase, sock, label) {
|
|
343
|
+
connectToHost(hostInfo, password, privateKey, passphrase, sock, label, keepaliveInterval, keepaliveCountMax) {
|
|
340
344
|
return new Promise((resolve, reject) => {
|
|
341
345
|
const client = new Client();
|
|
342
346
|
const sshConfig = {
|
|
@@ -356,6 +360,17 @@ var SSHTunnel = class {
|
|
|
356
360
|
if (sock) {
|
|
357
361
|
sshConfig.sock = sock;
|
|
358
362
|
}
|
|
363
|
+
if (keepaliveInterval !== void 0) {
|
|
364
|
+
if (Number.isNaN(keepaliveInterval) || keepaliveInterval < 0) {
|
|
365
|
+
const desc = label || `${hostInfo.host}:${hostInfo.port}`;
|
|
366
|
+
console.warn(
|
|
367
|
+
`Invalid SSH keepaliveInterval (${keepaliveInterval}) for ${desc}; keepalive configuration will be ignored.`
|
|
368
|
+
);
|
|
369
|
+
} else if (keepaliveInterval > 0) {
|
|
370
|
+
sshConfig.keepaliveInterval = keepaliveInterval * 1e3;
|
|
371
|
+
sshConfig.keepaliveCountMax = keepaliveCountMax ?? 3;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
359
374
|
const onError = (err) => {
|
|
360
375
|
client.removeListener("ready", onReady);
|
|
361
376
|
client.destroy();
|
|
@@ -1017,6 +1032,27 @@ function resolveSSHConfig() {
|
|
|
1017
1032
|
config.proxyJump = process.env.SSH_PROXY_JUMP;
|
|
1018
1033
|
sources.push("SSH_PROXY_JUMP from environment");
|
|
1019
1034
|
}
|
|
1035
|
+
const parseNonNegativeInteger = (value, name) => {
|
|
1036
|
+
const parsed = Number(value);
|
|
1037
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
1038
|
+
throw new Error(`Invalid value for ${name}: "${value}". Expected a non-negative integer.`);
|
|
1039
|
+
}
|
|
1040
|
+
return parsed;
|
|
1041
|
+
};
|
|
1042
|
+
if (args["ssh-keepalive-interval"]) {
|
|
1043
|
+
config.keepaliveInterval = parseNonNegativeInteger(args["ssh-keepalive-interval"], "ssh-keepalive-interval");
|
|
1044
|
+
sources.push("ssh-keepalive-interval from command line");
|
|
1045
|
+
} else if (process.env.SSH_KEEPALIVE_INTERVAL) {
|
|
1046
|
+
config.keepaliveInterval = parseNonNegativeInteger(process.env.SSH_KEEPALIVE_INTERVAL, "SSH_KEEPALIVE_INTERVAL");
|
|
1047
|
+
sources.push("SSH_KEEPALIVE_INTERVAL from environment");
|
|
1048
|
+
}
|
|
1049
|
+
if (args["ssh-keepalive-count-max"]) {
|
|
1050
|
+
config.keepaliveCountMax = parseNonNegativeInteger(args["ssh-keepalive-count-max"], "ssh-keepalive-count-max");
|
|
1051
|
+
sources.push("ssh-keepalive-count-max from command line");
|
|
1052
|
+
} else if (process.env.SSH_KEEPALIVE_COUNT_MAX) {
|
|
1053
|
+
config.keepaliveCountMax = parseNonNegativeInteger(process.env.SSH_KEEPALIVE_COUNT_MAX, "SSH_KEEPALIVE_COUNT_MAX");
|
|
1054
|
+
sources.push("SSH_KEEPALIVE_COUNT_MAX from environment");
|
|
1055
|
+
}
|
|
1020
1056
|
if (!config.host || !config.username) {
|
|
1021
1057
|
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
1022
1058
|
}
|
|
@@ -1096,6 +1132,8 @@ async function resolveSourceConfigs() {
|
|
|
1096
1132
|
source.ssh_password = sshResult.config.password;
|
|
1097
1133
|
source.ssh_key = sshResult.config.privateKey;
|
|
1098
1134
|
source.ssh_passphrase = sshResult.config.passphrase;
|
|
1135
|
+
source.ssh_keepalive_interval = sshResult.config.keepaliveInterval;
|
|
1136
|
+
source.ssh_keepalive_count_max = sshResult.config.keepaliveCountMax;
|
|
1099
1137
|
}
|
|
1100
1138
|
if (dsnResult.isDemo) {
|
|
1101
1139
|
const { getSqliteInMemorySetupSql } = await import("./demo-loader-PSMTLZ2T.js");
|
|
@@ -1345,6 +1383,18 @@ function validateSourceConfig(source, configPath) {
|
|
|
1345
1383
|
);
|
|
1346
1384
|
}
|
|
1347
1385
|
}
|
|
1386
|
+
if (source.search_path !== void 0) {
|
|
1387
|
+
if (source.type !== "postgres") {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`Configuration file ${configPath}: source '${source.id}' has 'search_path' but it is only supported for PostgreSQL sources.`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof source.search_path !== "string" || source.search_path.trim().length === 0) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`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").`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1348
1398
|
if (source.readonly !== void 0) {
|
|
1349
1399
|
throw new Error(
|
|
1350
1400
|
`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.`
|
|
@@ -1412,7 +1462,8 @@ function buildDSNFromSource(source) {
|
|
|
1412
1462
|
`Source '${source.id}': 'database' field is required for SQLite`
|
|
1413
1463
|
);
|
|
1414
1464
|
}
|
|
1415
|
-
|
|
1465
|
+
const prefix = source.database.startsWith("/") ? "sqlite://" : "sqlite:///";
|
|
1466
|
+
return `${prefix}${source.database}`;
|
|
1416
1467
|
}
|
|
1417
1468
|
if (source.type === "redis") {
|
|
1418
1469
|
const host = source.host || "localhost";
|
|
@@ -1596,7 +1647,9 @@ var ConnectorManager = class {
|
|
|
1596
1647
|
password: source.ssh_password,
|
|
1597
1648
|
privateKey: source.ssh_key,
|
|
1598
1649
|
passphrase: source.ssh_passphrase,
|
|
1599
|
-
proxyJump: source.ssh_proxy_jump
|
|
1650
|
+
proxyJump: source.ssh_proxy_jump,
|
|
1651
|
+
keepaliveInterval: source.ssh_keepalive_interval,
|
|
1652
|
+
keepaliveCountMax: source.ssh_keepalive_count_max
|
|
1600
1653
|
};
|
|
1601
1654
|
if (!sshConfig.password && !sshConfig.privateKey) {
|
|
1602
1655
|
throw new Error(
|
|
@@ -1637,6 +1690,9 @@ var ConnectorManager = class {
|
|
|
1637
1690
|
if (source.readonly !== void 0) {
|
|
1638
1691
|
config.readonly = source.readonly;
|
|
1639
1692
|
}
|
|
1693
|
+
if (source.search_path) {
|
|
1694
|
+
config.searchPath = source.search_path;
|
|
1695
|
+
}
|
|
1640
1696
|
await connector.connect(actualDSN, source.init_script, config);
|
|
1641
1697
|
this.connectors.set(sourceId, connector);
|
|
1642
1698
|
if (!this.sourceIds.includes(sourceId)) {
|
|
@@ -1772,60 +1828,213 @@ var ConnectorManager = class {
|
|
|
1772
1828
|
};
|
|
1773
1829
|
|
|
1774
1830
|
// src/utils/sql-parser.ts
|
|
1775
|
-
|
|
1776
|
-
|
|
1831
|
+
var TokenType = { Plain: 0, Comment: 1, QuotedBlock: 2 };
|
|
1832
|
+
function plainToken(i) {
|
|
1833
|
+
return { type: TokenType.Plain, end: i + 1 };
|
|
1834
|
+
}
|
|
1835
|
+
function scanSingleLineComment(sql, i) {
|
|
1836
|
+
if (sql[i] !== "-" || sql[i + 1] !== "-") {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
let j = i;
|
|
1840
|
+
while (j < sql.length && sql[j] !== "\n") {
|
|
1841
|
+
j++;
|
|
1842
|
+
}
|
|
1843
|
+
return { type: TokenType.Comment, end: j };
|
|
1844
|
+
}
|
|
1845
|
+
function scanMultiLineComment(sql, i) {
|
|
1846
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
1847
|
+
return null;
|
|
1848
|
+
}
|
|
1849
|
+
let j = i + 2;
|
|
1850
|
+
while (j < sql.length && !(sql[j] === "*" && sql[j + 1] === "/")) {
|
|
1851
|
+
j++;
|
|
1852
|
+
}
|
|
1853
|
+
if (j < sql.length) {
|
|
1854
|
+
j += 2;
|
|
1855
|
+
}
|
|
1856
|
+
return { type: TokenType.Comment, end: j };
|
|
1857
|
+
}
|
|
1858
|
+
function scanNestedMultiLineComment(sql, i) {
|
|
1859
|
+
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
let j = i + 2;
|
|
1863
|
+
let depth = 1;
|
|
1864
|
+
while (j < sql.length && depth > 0) {
|
|
1865
|
+
if (sql[j] === "/" && sql[j + 1] === "*") {
|
|
1866
|
+
depth++;
|
|
1867
|
+
j += 2;
|
|
1868
|
+
} else if (sql[j] === "*" && sql[j + 1] === "/") {
|
|
1869
|
+
depth--;
|
|
1870
|
+
j += 2;
|
|
1871
|
+
} else {
|
|
1872
|
+
j++;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return { type: TokenType.Comment, end: j };
|
|
1876
|
+
}
|
|
1877
|
+
function scanSingleQuotedString(sql, i) {
|
|
1878
|
+
if (sql[i] !== "'") {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
let j = i + 1;
|
|
1882
|
+
while (j < sql.length) {
|
|
1883
|
+
if (sql[j] === "'" && sql[j + 1] === "'") {
|
|
1884
|
+
j += 2;
|
|
1885
|
+
} else if (sql[j] === "'") {
|
|
1886
|
+
j++;
|
|
1887
|
+
break;
|
|
1888
|
+
} else {
|
|
1889
|
+
j++;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
1893
|
+
}
|
|
1894
|
+
function scanDoubleQuotedString(sql, i) {
|
|
1895
|
+
if (sql[i] !== '"') {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
let j = i + 1;
|
|
1899
|
+
while (j < sql.length) {
|
|
1900
|
+
if (sql[j] === '"' && sql[j + 1] === '"') {
|
|
1901
|
+
j += 2;
|
|
1902
|
+
} else if (sql[j] === '"') {
|
|
1903
|
+
j++;
|
|
1904
|
+
break;
|
|
1905
|
+
} else {
|
|
1906
|
+
j++;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
1910
|
+
}
|
|
1911
|
+
var dollarQuoteOpenRegex = /^\$([a-zA-Z_]\w*)?\$/;
|
|
1912
|
+
function scanDollarQuotedBlock(sql, i) {
|
|
1913
|
+
if (sql[i] !== "$") {
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
const next = sql[i + 1];
|
|
1917
|
+
if (next >= "0" && next <= "9") {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
const remaining = sql.substring(i);
|
|
1921
|
+
const m = dollarQuoteOpenRegex.exec(remaining);
|
|
1922
|
+
if (!m) {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
const tag = m[0];
|
|
1926
|
+
const bodyStart = i + tag.length;
|
|
1927
|
+
const closeIdx = sql.indexOf(tag, bodyStart);
|
|
1928
|
+
const end = closeIdx !== -1 ? closeIdx + tag.length : sql.length;
|
|
1929
|
+
return { type: TokenType.QuotedBlock, end };
|
|
1930
|
+
}
|
|
1931
|
+
function scanBacktickQuotedIdentifier(sql, i) {
|
|
1932
|
+
if (sql[i] !== "`") {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
let j = i + 1;
|
|
1936
|
+
while (j < sql.length) {
|
|
1937
|
+
if (sql[j] === "`" && sql[j + 1] === "`") {
|
|
1938
|
+
j += 2;
|
|
1939
|
+
} else if (sql[j] === "`") {
|
|
1940
|
+
j++;
|
|
1941
|
+
break;
|
|
1942
|
+
} else {
|
|
1943
|
+
j++;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
1947
|
+
}
|
|
1948
|
+
function scanBracketQuotedIdentifier(sql, i) {
|
|
1949
|
+
if (sql[i] !== "[") {
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
let j = i + 1;
|
|
1953
|
+
while (j < sql.length) {
|
|
1954
|
+
if (sql[j] === "]" && sql[j + 1] === "]") {
|
|
1955
|
+
j += 2;
|
|
1956
|
+
} else if (sql[j] === "]") {
|
|
1957
|
+
j++;
|
|
1958
|
+
break;
|
|
1959
|
+
} else {
|
|
1960
|
+
j++;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return { type: TokenType.QuotedBlock, end: j };
|
|
1964
|
+
}
|
|
1965
|
+
function scanTokenAnsi(sql, i) {
|
|
1966
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
|
|
1967
|
+
}
|
|
1968
|
+
function scanTokenPostgres(sql, i) {
|
|
1969
|
+
return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
|
|
1970
|
+
}
|
|
1971
|
+
function scanTokenMySQL(sql, i) {
|
|
1972
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
1973
|
+
}
|
|
1974
|
+
function scanTokenSQLite(sql, i) {
|
|
1975
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
1976
|
+
}
|
|
1977
|
+
function scanTokenSQLServer(sql, i) {
|
|
1978
|
+
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
1979
|
+
}
|
|
1980
|
+
var dialectScanners = {
|
|
1981
|
+
postgres: scanTokenPostgres,
|
|
1982
|
+
mysql: scanTokenMySQL,
|
|
1983
|
+
mariadb: scanTokenMySQL,
|
|
1984
|
+
sqlite: scanTokenSQLite,
|
|
1985
|
+
sqlserver: scanTokenSQLServer
|
|
1986
|
+
};
|
|
1987
|
+
function getScanner(dialect) {
|
|
1988
|
+
return dialect ? dialectScanners[dialect] ?? scanTokenAnsi : scanTokenAnsi;
|
|
1989
|
+
}
|
|
1990
|
+
function stripCommentsAndStrings(sql, dialect) {
|
|
1991
|
+
const scanToken = getScanner(dialect);
|
|
1992
|
+
const parts = [];
|
|
1993
|
+
let plainStart = -1;
|
|
1777
1994
|
let i = 0;
|
|
1778
1995
|
while (i < sql.length) {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1996
|
+
const token = scanToken(sql, i);
|
|
1997
|
+
if (token.type === TokenType.Plain) {
|
|
1998
|
+
if (plainStart === -1) {
|
|
1999
|
+
plainStart = i;
|
|
1782
2000
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
i += 2;
|
|
1788
|
-
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
|
|
1789
|
-
i++;
|
|
2001
|
+
} else {
|
|
2002
|
+
if (plainStart !== -1) {
|
|
2003
|
+
parts.push(sql.substring(plainStart, i));
|
|
2004
|
+
plainStart = -1;
|
|
1790
2005
|
}
|
|
1791
|
-
|
|
1792
|
-
result += " ";
|
|
1793
|
-
continue;
|
|
2006
|
+
parts.push(" ");
|
|
1794
2007
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2008
|
+
i = token.end;
|
|
2009
|
+
}
|
|
2010
|
+
if (plainStart !== -1) {
|
|
2011
|
+
parts.push(sql.substring(plainStart));
|
|
2012
|
+
}
|
|
2013
|
+
return parts.join("");
|
|
2014
|
+
}
|
|
2015
|
+
function splitSQLStatements(sql, dialect) {
|
|
2016
|
+
const scanToken = getScanner(dialect);
|
|
2017
|
+
const statements = [];
|
|
2018
|
+
let stmtStart = 0;
|
|
2019
|
+
let i = 0;
|
|
2020
|
+
while (i < sql.length) {
|
|
2021
|
+
if (sql[i] === ";") {
|
|
2022
|
+
const trimmed2 = sql.substring(stmtStart, i).trim();
|
|
2023
|
+
if (trimmed2.length > 0) {
|
|
2024
|
+
statements.push(trimmed2);
|
|
1806
2025
|
}
|
|
1807
|
-
|
|
1808
|
-
continue;
|
|
1809
|
-
}
|
|
1810
|
-
if (sql[i] === '"') {
|
|
2026
|
+
stmtStart = i + 1;
|
|
1811
2027
|
i++;
|
|
1812
|
-
while (i < sql.length) {
|
|
1813
|
-
if (sql[i] === '"' && sql[i + 1] === '"') {
|
|
1814
|
-
i += 2;
|
|
1815
|
-
} else if (sql[i] === '"') {
|
|
1816
|
-
i++;
|
|
1817
|
-
break;
|
|
1818
|
-
} else {
|
|
1819
|
-
i++;
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
result += " ";
|
|
1823
2028
|
continue;
|
|
1824
2029
|
}
|
|
1825
|
-
|
|
1826
|
-
i
|
|
2030
|
+
const token = scanToken(sql, i);
|
|
2031
|
+
i = token.end;
|
|
2032
|
+
}
|
|
2033
|
+
const trimmed = sql.substring(stmtStart).trim();
|
|
2034
|
+
if (trimmed.length > 0) {
|
|
2035
|
+
statements.push(trimmed);
|
|
1827
2036
|
}
|
|
1828
|
-
return
|
|
2037
|
+
return statements;
|
|
1829
2038
|
}
|
|
1830
2039
|
|
|
1831
2040
|
// src/utils/parameter-mapper.ts
|
|
@@ -2172,6 +2381,7 @@ export {
|
|
|
2172
2381
|
getDatabaseTypeFromDSN,
|
|
2173
2382
|
getDefaultPortForType,
|
|
2174
2383
|
stripCommentsAndStrings,
|
|
2384
|
+
splitSQLStatements,
|
|
2175
2385
|
isDemoMode,
|
|
2176
2386
|
resolveTransport,
|
|
2177
2387
|
resolvePort,
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
resolvePort,
|
|
18
18
|
resolveSourceConfigs,
|
|
19
19
|
resolveTransport,
|
|
20
|
+
splitSQLStatements,
|
|
20
21
|
stripCommentsAndStrings
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-KAW6SKAC.js";
|
|
22
23
|
|
|
23
24
|
// src/connectors/postgres/index.ts
|
|
24
25
|
import pg from "pg";
|
|
@@ -143,6 +144,36 @@ var SQLRowLimiter = class {
|
|
|
143
144
|
}
|
|
144
145
|
};
|
|
145
146
|
|
|
147
|
+
// src/utils/identifier-quoter.ts
|
|
148
|
+
function quoteIdentifier(identifier, dbType) {
|
|
149
|
+
if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
|
|
150
|
+
throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
|
|
151
|
+
}
|
|
152
|
+
if (!identifier) {
|
|
153
|
+
throw new Error("Identifier cannot be empty");
|
|
154
|
+
}
|
|
155
|
+
switch (dbType) {
|
|
156
|
+
case "postgres":
|
|
157
|
+
case "sqlite":
|
|
158
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
159
|
+
case "mysql":
|
|
160
|
+
case "mariadb":
|
|
161
|
+
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
162
|
+
case "sqlserver":
|
|
163
|
+
return `[${identifier.replace(/]/g, "]]")}]`;
|
|
164
|
+
default:
|
|
165
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
|
|
169
|
+
const quotedTable = quoteIdentifier(tableName, dbType);
|
|
170
|
+
if (schemaName) {
|
|
171
|
+
const quotedSchema = quoteIdentifier(schemaName, dbType);
|
|
172
|
+
return `${quotedSchema}.${quotedTable}`;
|
|
173
|
+
}
|
|
174
|
+
return quotedTable;
|
|
175
|
+
}
|
|
176
|
+
|
|
146
177
|
// src/connectors/postgres/index.ts
|
|
147
178
|
var { Pool } = pg;
|
|
148
179
|
var PostgresDSNParser = class {
|
|
@@ -211,6 +242,8 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
211
242
|
this.pool = null;
|
|
212
243
|
// Source ID is set by ConnectorManager after cloning
|
|
213
244
|
this.sourceId = "default";
|
|
245
|
+
// Default schema for discovery methods (first entry from search_path, or "public")
|
|
246
|
+
this.defaultSchema = "public";
|
|
214
247
|
}
|
|
215
248
|
getId() {
|
|
216
249
|
return this.sourceId;
|
|
@@ -219,11 +252,21 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
219
252
|
return new _PostgresConnector();
|
|
220
253
|
}
|
|
221
254
|
async connect(dsn, initScript, config) {
|
|
255
|
+
this.defaultSchema = "public";
|
|
222
256
|
try {
|
|
223
257
|
const poolConfig = await this.dsnParser.parse(dsn, config);
|
|
224
258
|
if (config?.readonly) {
|
|
225
259
|
poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
|
|
226
260
|
}
|
|
261
|
+
if (config?.searchPath) {
|
|
262
|
+
const schemas = config.searchPath.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
263
|
+
if (schemas.length > 0) {
|
|
264
|
+
this.defaultSchema = schemas[0];
|
|
265
|
+
const quotedSchemas = schemas.map((s) => quoteIdentifier(s, "postgres"));
|
|
266
|
+
const optionsValue = quotedSchemas.join(",").replace(/\\/g, "\\\\").replace(/ /g, "\\ ");
|
|
267
|
+
poolConfig.options = (poolConfig.options || "") + ` -c search_path=${optionsValue}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
227
270
|
this.pool = new Pool(poolConfig);
|
|
228
271
|
const client = await this.pool.connect();
|
|
229
272
|
client.release();
|
|
@@ -261,7 +304,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
261
304
|
}
|
|
262
305
|
const client = await this.pool.connect();
|
|
263
306
|
try {
|
|
264
|
-
const schemaToUse = schema ||
|
|
307
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
265
308
|
const result = await client.query(
|
|
266
309
|
`
|
|
267
310
|
SELECT table_name
|
|
@@ -282,7 +325,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
282
325
|
}
|
|
283
326
|
const client = await this.pool.connect();
|
|
284
327
|
try {
|
|
285
|
-
const schemaToUse = schema ||
|
|
328
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
286
329
|
const result = await client.query(
|
|
287
330
|
`
|
|
288
331
|
SELECT EXISTS (
|
|
@@ -304,7 +347,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
304
347
|
}
|
|
305
348
|
const client = await this.pool.connect();
|
|
306
349
|
try {
|
|
307
|
-
const schemaToUse = schema ||
|
|
350
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
308
351
|
const result = await client.query(
|
|
309
352
|
`
|
|
310
353
|
SELECT
|
|
@@ -352,18 +395,27 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
352
395
|
}
|
|
353
396
|
const client = await this.pool.connect();
|
|
354
397
|
try {
|
|
355
|
-
const schemaToUse = schema ||
|
|
398
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
356
399
|
const result = await client.query(
|
|
357
400
|
`
|
|
358
|
-
SELECT
|
|
359
|
-
column_name,
|
|
360
|
-
data_type,
|
|
361
|
-
is_nullable,
|
|
362
|
-
column_default
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
401
|
+
SELECT
|
|
402
|
+
c.column_name,
|
|
403
|
+
c.data_type,
|
|
404
|
+
c.is_nullable,
|
|
405
|
+
c.column_default,
|
|
406
|
+
pgd.description
|
|
407
|
+
FROM information_schema.columns c
|
|
408
|
+
LEFT JOIN pg_catalog.pg_namespace nsp
|
|
409
|
+
ON nsp.nspname = c.table_schema
|
|
410
|
+
LEFT JOIN pg_catalog.pg_class cls
|
|
411
|
+
ON cls.relnamespace = nsp.oid
|
|
412
|
+
AND cls.relname = c.table_name
|
|
413
|
+
LEFT JOIN pg_catalog.pg_description pgd
|
|
414
|
+
ON pgd.objoid = cls.oid
|
|
415
|
+
AND pgd.objsubid = c.ordinal_position
|
|
416
|
+
WHERE c.table_schema = $1
|
|
417
|
+
AND c.table_name = $2
|
|
418
|
+
ORDER BY c.ordinal_position
|
|
367
419
|
`,
|
|
368
420
|
[schemaToUse, tableName]
|
|
369
421
|
);
|
|
@@ -372,22 +424,82 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
372
424
|
client.release();
|
|
373
425
|
}
|
|
374
426
|
}
|
|
375
|
-
async
|
|
427
|
+
async getTableRowCount(tableName, schema) {
|
|
376
428
|
if (!this.pool) {
|
|
377
429
|
throw new Error("Not connected to database");
|
|
378
430
|
}
|
|
379
431
|
const client = await this.pool.connect();
|
|
380
432
|
try {
|
|
381
|
-
const schemaToUse = schema ||
|
|
433
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
382
434
|
const result = await client.query(
|
|
383
435
|
`
|
|
384
|
-
SELECT
|
|
436
|
+
SELECT c.reltuples::bigint as count
|
|
437
|
+
FROM pg_class c
|
|
438
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
439
|
+
WHERE c.relname = $1
|
|
440
|
+
AND n.nspname = $2
|
|
441
|
+
AND c.relkind IN ('r','p','m','f')
|
|
442
|
+
`,
|
|
443
|
+
[tableName, schemaToUse]
|
|
444
|
+
);
|
|
445
|
+
if (result.rows.length > 0) {
|
|
446
|
+
const count = Number(result.rows[0].count);
|
|
447
|
+
return count >= 0 ? count : null;
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
} finally {
|
|
451
|
+
client.release();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async getTableComment(tableName, schema) {
|
|
455
|
+
if (!this.pool) {
|
|
456
|
+
throw new Error("Not connected to database");
|
|
457
|
+
}
|
|
458
|
+
const client = await this.pool.connect();
|
|
459
|
+
try {
|
|
460
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
461
|
+
const result = await client.query(
|
|
462
|
+
`
|
|
463
|
+
SELECT obj_description(c.oid) as table_comment
|
|
464
|
+
FROM pg_catalog.pg_class c
|
|
465
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
466
|
+
WHERE c.relname = $1
|
|
467
|
+
AND n.nspname = $2
|
|
468
|
+
AND c.relkind IN ('r','p','m','f')
|
|
469
|
+
`,
|
|
470
|
+
[tableName, schemaToUse]
|
|
471
|
+
);
|
|
472
|
+
if (result.rows.length > 0) {
|
|
473
|
+
return result.rows[0].table_comment || null;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
} finally {
|
|
477
|
+
client.release();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async getStoredProcedures(schema, routineType) {
|
|
481
|
+
if (!this.pool) {
|
|
482
|
+
throw new Error("Not connected to database");
|
|
483
|
+
}
|
|
484
|
+
const client = await this.pool.connect();
|
|
485
|
+
try {
|
|
486
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
487
|
+
const params = [schemaToUse];
|
|
488
|
+
let typeFilter = "";
|
|
489
|
+
if (routineType === "function") {
|
|
490
|
+
typeFilter = " AND routine_type = 'FUNCTION'";
|
|
491
|
+
} else if (routineType === "procedure") {
|
|
492
|
+
typeFilter = " AND routine_type = 'PROCEDURE'";
|
|
493
|
+
}
|
|
494
|
+
const result = await client.query(
|
|
495
|
+
`
|
|
496
|
+
SELECT
|
|
385
497
|
routine_name
|
|
386
498
|
FROM information_schema.routines
|
|
387
|
-
WHERE routine_schema = $1
|
|
499
|
+
WHERE routine_schema = $1${typeFilter}
|
|
388
500
|
ORDER BY routine_name
|
|
389
501
|
`,
|
|
390
|
-
|
|
502
|
+
params
|
|
391
503
|
);
|
|
392
504
|
return result.rows.map((row) => row.routine_name);
|
|
393
505
|
} finally {
|
|
@@ -400,7 +512,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
400
512
|
}
|
|
401
513
|
const client = await this.pool.connect();
|
|
402
514
|
try {
|
|
403
|
-
const schemaToUse = schema ||
|
|
515
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
404
516
|
const result = await client.query(
|
|
405
517
|
`
|
|
406
518
|
SELECT
|
|
@@ -476,7 +588,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
476
588
|
}
|
|
477
589
|
const client = await this.pool.connect();
|
|
478
590
|
try {
|
|
479
|
-
const statements = sql2
|
|
591
|
+
const statements = splitSQLStatements(sql2, "postgres");
|
|
480
592
|
if (statements.length === 1) {
|
|
481
593
|
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
482
594
|
let result;
|
|
@@ -791,33 +903,78 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
791
903
|
const schemaToUse = schema || "dbo";
|
|
792
904
|
const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
|
|
793
905
|
const query = `
|
|
794
|
-
SELECT COLUMN_NAME as column_name,
|
|
795
|
-
DATA_TYPE as data_type,
|
|
796
|
-
IS_NULLABLE as is_nullable,
|
|
797
|
-
COLUMN_DEFAULT as column_default
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
906
|
+
SELECT c.COLUMN_NAME as column_name,
|
|
907
|
+
c.DATA_TYPE as data_type,
|
|
908
|
+
c.IS_NULLABLE as is_nullable,
|
|
909
|
+
c.COLUMN_DEFAULT as column_default,
|
|
910
|
+
ep.value as description
|
|
911
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
912
|
+
LEFT JOIN sys.columns sc
|
|
913
|
+
ON sc.name = c.COLUMN_NAME
|
|
914
|
+
AND sc.object_id = OBJECT_ID(QUOTENAME(c.TABLE_SCHEMA) + '.' + QUOTENAME(c.TABLE_NAME))
|
|
915
|
+
LEFT JOIN sys.extended_properties ep
|
|
916
|
+
ON ep.major_id = sc.object_id
|
|
917
|
+
AND ep.minor_id = sc.column_id
|
|
918
|
+
AND ep.name = 'MS_Description'
|
|
919
|
+
WHERE c.TABLE_NAME = @tableName
|
|
920
|
+
AND c.TABLE_SCHEMA = @schema
|
|
921
|
+
ORDER BY c.ORDINAL_POSITION
|
|
802
922
|
`;
|
|
803
923
|
const result = await request.query(query);
|
|
804
|
-
return result.recordset
|
|
924
|
+
return result.recordset.map((row) => ({
|
|
925
|
+
...row,
|
|
926
|
+
description: row.description || null
|
|
927
|
+
}));
|
|
805
928
|
} catch (error) {
|
|
806
929
|
throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
|
|
807
930
|
}
|
|
808
931
|
}
|
|
809
|
-
async
|
|
932
|
+
async getTableComment(tableName, schema) {
|
|
933
|
+
if (!this.connection) {
|
|
934
|
+
throw new Error("Not connected to SQL Server database");
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
const schemaToUse = schema || "dbo";
|
|
938
|
+
const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
|
|
939
|
+
const query = `
|
|
940
|
+
SELECT ep.value as table_comment
|
|
941
|
+
FROM sys.extended_properties ep
|
|
942
|
+
JOIN sys.tables t ON ep.major_id = t.object_id
|
|
943
|
+
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
944
|
+
WHERE ep.minor_id = 0
|
|
945
|
+
AND ep.name = 'MS_Description'
|
|
946
|
+
AND t.name = @tableName
|
|
947
|
+
AND s.name = @schema
|
|
948
|
+
`;
|
|
949
|
+
const result = await request.query(query);
|
|
950
|
+
if (result.recordset.length > 0) {
|
|
951
|
+
return result.recordset[0].table_comment || null;
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
} catch (error) {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async getStoredProcedures(schema, routineType) {
|
|
810
959
|
if (!this.connection) {
|
|
811
960
|
throw new Error("Not connected to SQL Server database");
|
|
812
961
|
}
|
|
813
962
|
try {
|
|
814
963
|
const schemaToUse = schema || "dbo";
|
|
815
964
|
const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
|
|
965
|
+
let typeFilter;
|
|
966
|
+
if (routineType === "function") {
|
|
967
|
+
typeFilter = "AND ROUTINE_TYPE = 'FUNCTION'";
|
|
968
|
+
} else if (routineType === "procedure") {
|
|
969
|
+
typeFilter = "AND ROUTINE_TYPE = 'PROCEDURE'";
|
|
970
|
+
} else {
|
|
971
|
+
typeFilter = "AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')";
|
|
972
|
+
}
|
|
816
973
|
const query = `
|
|
817
974
|
SELECT ROUTINE_NAME
|
|
818
975
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
819
976
|
WHERE ROUTINE_SCHEMA = @schema
|
|
820
|
-
|
|
977
|
+
${typeFilter}
|
|
821
978
|
ORDER BY ROUTINE_NAME
|
|
822
979
|
`;
|
|
823
980
|
const result = await request.query(query);
|
|
@@ -950,38 +1107,6 @@ ConnectorRegistry.register(sqlServerConnector);
|
|
|
950
1107
|
|
|
951
1108
|
// src/connectors/sqlite/index.ts
|
|
952
1109
|
import Database from "better-sqlite3";
|
|
953
|
-
|
|
954
|
-
// src/utils/identifier-quoter.ts
|
|
955
|
-
function quoteIdentifier(identifier, dbType) {
|
|
956
|
-
if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
|
|
957
|
-
throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
|
|
958
|
-
}
|
|
959
|
-
if (!identifier) {
|
|
960
|
-
throw new Error("Identifier cannot be empty");
|
|
961
|
-
}
|
|
962
|
-
switch (dbType) {
|
|
963
|
-
case "postgres":
|
|
964
|
-
case "sqlite":
|
|
965
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
966
|
-
case "mysql":
|
|
967
|
-
case "mariadb":
|
|
968
|
-
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
969
|
-
case "sqlserver":
|
|
970
|
-
return `[${identifier.replace(/]/g, "]]")}]`;
|
|
971
|
-
default:
|
|
972
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
|
|
976
|
-
const quotedTable = quoteIdentifier(tableName, dbType);
|
|
977
|
-
if (schemaName) {
|
|
978
|
-
const quotedSchema = quoteIdentifier(schemaName, dbType);
|
|
979
|
-
return `${quotedSchema}.${quotedTable}`;
|
|
980
|
-
}
|
|
981
|
-
return quotedTable;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// src/connectors/sqlite/index.ts
|
|
985
1110
|
var SQLiteDSNParser = class {
|
|
986
1111
|
async parse(dsn, config) {
|
|
987
1112
|
if (!this.isValidDSN(dsn)) {
|
|
@@ -1183,14 +1308,15 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1183
1308
|
data_type: row.type,
|
|
1184
1309
|
// In SQLite, primary key columns are automatically NOT NULL even if notnull=0
|
|
1185
1310
|
is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
|
|
1186
|
-
column_default: row.dflt_value
|
|
1311
|
+
column_default: row.dflt_value,
|
|
1312
|
+
description: null
|
|
1187
1313
|
}));
|
|
1188
1314
|
return columns;
|
|
1189
1315
|
} catch (error) {
|
|
1190
1316
|
throw error;
|
|
1191
1317
|
}
|
|
1192
1318
|
}
|
|
1193
|
-
async getStoredProcedures(schema) {
|
|
1319
|
+
async getStoredProcedures(schema, routineType) {
|
|
1194
1320
|
if (!this.db) {
|
|
1195
1321
|
throw new Error("Not connected to SQLite database");
|
|
1196
1322
|
}
|
|
@@ -1209,7 +1335,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1209
1335
|
throw new Error("Not connected to SQLite database");
|
|
1210
1336
|
}
|
|
1211
1337
|
try {
|
|
1212
|
-
const statements = sql2
|
|
1338
|
+
const statements = splitSQLStatements(sql2, "sqlite");
|
|
1213
1339
|
if (statements.length === 1) {
|
|
1214
1340
|
let processedStatement = statements[0];
|
|
1215
1341
|
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
@@ -1568,11 +1694,12 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1568
1694
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1569
1695
|
const [rows] = await this.pool.query(
|
|
1570
1696
|
`
|
|
1571
|
-
SELECT
|
|
1572
|
-
COLUMN_NAME as column_name,
|
|
1573
|
-
DATA_TYPE as data_type,
|
|
1697
|
+
SELECT
|
|
1698
|
+
COLUMN_NAME as column_name,
|
|
1699
|
+
DATA_TYPE as data_type,
|
|
1574
1700
|
IS_NULLABLE as is_nullable,
|
|
1575
|
-
COLUMN_DEFAULT as column_default
|
|
1701
|
+
COLUMN_DEFAULT as column_default,
|
|
1702
|
+
COLUMN_COMMENT as description
|
|
1576
1703
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1577
1704
|
${schemaClause}
|
|
1578
1705
|
AND TABLE_NAME = ?
|
|
@@ -1580,24 +1707,57 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1580
1707
|
`,
|
|
1581
1708
|
queryParams
|
|
1582
1709
|
);
|
|
1583
|
-
return rows
|
|
1710
|
+
return rows.map((row) => ({
|
|
1711
|
+
...row,
|
|
1712
|
+
description: row.description || null
|
|
1713
|
+
}));
|
|
1584
1714
|
} catch (error) {
|
|
1585
1715
|
console.error("Error getting table schema:", error);
|
|
1586
1716
|
throw error;
|
|
1587
1717
|
}
|
|
1588
1718
|
}
|
|
1589
|
-
async
|
|
1719
|
+
async getTableComment(tableName, schema) {
|
|
1720
|
+
if (!this.pool) {
|
|
1721
|
+
throw new Error("Not connected to database");
|
|
1722
|
+
}
|
|
1723
|
+
try {
|
|
1724
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1725
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1726
|
+
const [rows] = await this.pool.query(
|
|
1727
|
+
`
|
|
1728
|
+
SELECT TABLE_COMMENT
|
|
1729
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1730
|
+
${schemaClause}
|
|
1731
|
+
AND TABLE_NAME = ?
|
|
1732
|
+
`,
|
|
1733
|
+
queryParams
|
|
1734
|
+
);
|
|
1735
|
+
if (rows.length > 0) {
|
|
1736
|
+
return rows[0].TABLE_COMMENT || null;
|
|
1737
|
+
}
|
|
1738
|
+
return null;
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async getStoredProcedures(schema, routineType) {
|
|
1590
1744
|
if (!this.pool) {
|
|
1591
1745
|
throw new Error("Not connected to database");
|
|
1592
1746
|
}
|
|
1593
1747
|
try {
|
|
1594
1748
|
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
1595
1749
|
const queryParams = schema ? [schema] : [];
|
|
1750
|
+
let typeFilter = "";
|
|
1751
|
+
if (routineType === "function") {
|
|
1752
|
+
typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
|
|
1753
|
+
} else if (routineType === "procedure") {
|
|
1754
|
+
typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
|
|
1755
|
+
}
|
|
1596
1756
|
const [rows] = await this.pool.query(
|
|
1597
1757
|
`
|
|
1598
1758
|
SELECT ROUTINE_NAME
|
|
1599
1759
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1600
|
-
${schemaClause}
|
|
1760
|
+
${schemaClause}${typeFilter}
|
|
1601
1761
|
ORDER BY ROUTINE_NAME
|
|
1602
1762
|
`,
|
|
1603
1763
|
queryParams
|
|
@@ -1721,7 +1881,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1721
1881
|
try {
|
|
1722
1882
|
let processedSQL = sql2;
|
|
1723
1883
|
if (options.maxRows) {
|
|
1724
|
-
const statements = sql2
|
|
1884
|
+
const statements = splitSQLStatements(sql2, "mysql");
|
|
1725
1885
|
const processedStatements = statements.map(
|
|
1726
1886
|
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
1727
1887
|
);
|
|
@@ -1759,7 +1919,7 @@ var mysqlConnector = new MySQLConnector();
|
|
|
1759
1919
|
ConnectorRegistry.register(mysqlConnector);
|
|
1760
1920
|
|
|
1761
1921
|
// src/connectors/mariadb/index.ts
|
|
1762
|
-
import
|
|
1922
|
+
import { createPool } from "mariadb";
|
|
1763
1923
|
var MariadbDSNParser = class {
|
|
1764
1924
|
async parse(dsn, config) {
|
|
1765
1925
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
@@ -1843,7 +2003,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1843
2003
|
async connect(dsn, initScript, config) {
|
|
1844
2004
|
try {
|
|
1845
2005
|
const connectionConfig = await this.dsnParser.parse(dsn, config);
|
|
1846
|
-
this.pool =
|
|
2006
|
+
this.pool = createPool(connectionConfig);
|
|
1847
2007
|
await this.pool.query("SELECT 1");
|
|
1848
2008
|
} catch (err) {
|
|
1849
2009
|
console.error("Failed to connect to MariaDB database:", err);
|
|
@@ -1981,11 +2141,12 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1981
2141
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1982
2142
|
const rows = await this.pool.query(
|
|
1983
2143
|
`
|
|
1984
|
-
SELECT
|
|
1985
|
-
COLUMN_NAME as column_name,
|
|
1986
|
-
DATA_TYPE as data_type,
|
|
2144
|
+
SELECT
|
|
2145
|
+
COLUMN_NAME as column_name,
|
|
2146
|
+
DATA_TYPE as data_type,
|
|
1987
2147
|
IS_NULLABLE as is_nullable,
|
|
1988
|
-
COLUMN_DEFAULT as column_default
|
|
2148
|
+
COLUMN_DEFAULT as column_default,
|
|
2149
|
+
COLUMN_COMMENT as description
|
|
1989
2150
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1990
2151
|
${schemaClause}
|
|
1991
2152
|
AND TABLE_NAME = ?
|
|
@@ -1993,24 +2154,57 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1993
2154
|
`,
|
|
1994
2155
|
queryParams
|
|
1995
2156
|
);
|
|
1996
|
-
return rows
|
|
2157
|
+
return rows.map((row) => ({
|
|
2158
|
+
...row,
|
|
2159
|
+
description: row.description || null
|
|
2160
|
+
}));
|
|
1997
2161
|
} catch (error) {
|
|
1998
2162
|
console.error("Error getting table schema:", error);
|
|
1999
2163
|
throw error;
|
|
2000
2164
|
}
|
|
2001
2165
|
}
|
|
2002
|
-
async
|
|
2166
|
+
async getTableComment(tableName, schema) {
|
|
2167
|
+
if (!this.pool) {
|
|
2168
|
+
throw new Error("Not connected to database");
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
2172
|
+
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
2173
|
+
const rows = await this.pool.query(
|
|
2174
|
+
`
|
|
2175
|
+
SELECT TABLE_COMMENT
|
|
2176
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
2177
|
+
${schemaClause}
|
|
2178
|
+
AND TABLE_NAME = ?
|
|
2179
|
+
`,
|
|
2180
|
+
queryParams
|
|
2181
|
+
);
|
|
2182
|
+
if (rows.length > 0) {
|
|
2183
|
+
return rows[0].TABLE_COMMENT || null;
|
|
2184
|
+
}
|
|
2185
|
+
return null;
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
async getStoredProcedures(schema, routineType) {
|
|
2003
2191
|
if (!this.pool) {
|
|
2004
2192
|
throw new Error("Not connected to database");
|
|
2005
2193
|
}
|
|
2006
2194
|
try {
|
|
2007
2195
|
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
2008
2196
|
const queryParams = schema ? [schema] : [];
|
|
2197
|
+
let typeFilter = "";
|
|
2198
|
+
if (routineType === "function") {
|
|
2199
|
+
typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
|
|
2200
|
+
} else if (routineType === "procedure") {
|
|
2201
|
+
typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
|
|
2202
|
+
}
|
|
2009
2203
|
const rows = await this.pool.query(
|
|
2010
2204
|
`
|
|
2011
2205
|
SELECT ROUTINE_NAME
|
|
2012
2206
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
2013
|
-
${schemaClause}
|
|
2207
|
+
${schemaClause}${typeFilter}
|
|
2014
2208
|
ORDER BY ROUTINE_NAME
|
|
2015
2209
|
`,
|
|
2016
2210
|
queryParams
|
|
@@ -2134,7 +2328,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
2134
2328
|
try {
|
|
2135
2329
|
let processedSQL = sql2;
|
|
2136
2330
|
if (options.maxRows) {
|
|
2137
|
-
const statements = sql2
|
|
2331
|
+
const statements = splitSQLStatements(sql2, "mariadb");
|
|
2138
2332
|
const processedStatements = statements.map(
|
|
2139
2333
|
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
2140
2334
|
);
|
|
@@ -2834,7 +3028,7 @@ var allowedKeywords = {
|
|
|
2834
3028
|
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2835
3029
|
};
|
|
2836
3030
|
function isReadOnlySQL(sql2, connectorType) {
|
|
2837
|
-
const cleanedSQL = stripCommentsAndStrings(sql2).trim().toLowerCase();
|
|
3031
|
+
const cleanedSQL = stripCommentsAndStrings(sql2, connectorType).trim().toLowerCase();
|
|
2838
3032
|
if (!cleanedSQL) {
|
|
2839
3033
|
return true;
|
|
2840
3034
|
}
|
|
@@ -2927,11 +3121,8 @@ function trackToolRequest(metadata, startTime, extra, success, error) {
|
|
|
2927
3121
|
var executeSqlSchema = {
|
|
2928
3122
|
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
2929
3123
|
};
|
|
2930
|
-
function splitSQLStatements(sql2) {
|
|
2931
|
-
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
2932
|
-
}
|
|
2933
3124
|
function areAllStatementsReadOnly(sql2, connectorType) {
|
|
2934
|
-
const statements = splitSQLStatements(sql2);
|
|
3125
|
+
const statements = splitSQLStatements(sql2, connectorType);
|
|
2935
3126
|
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
2936
3127
|
}
|
|
2937
3128
|
function createExecuteSqlToolHandler(sourceId) {
|
|
@@ -2988,7 +3179,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
2988
3179
|
// src/tools/search-objects.ts
|
|
2989
3180
|
import { z as z2 } from "zod";
|
|
2990
3181
|
var searchDatabaseObjectsSchema = {
|
|
2991
|
-
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Object type to search"),
|
|
3182
|
+
object_type: z2.enum(["schema", "table", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
2992
3183
|
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
|
|
2993
3184
|
schema: z2.string().optional().describe("Filter to schema"),
|
|
2994
3185
|
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
@@ -3001,6 +3192,9 @@ function likePatternToRegex(pattern) {
|
|
|
3001
3192
|
}
|
|
3002
3193
|
async function getTableRowCount(connector, tableName, schemaName) {
|
|
3003
3194
|
try {
|
|
3195
|
+
if (connector.getTableRowCount) {
|
|
3196
|
+
return await connector.getTableRowCount(tableName, schemaName);
|
|
3197
|
+
}
|
|
3004
3198
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
3005
3199
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
3006
3200
|
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
@@ -3012,6 +3206,16 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
3012
3206
|
}
|
|
3013
3207
|
return null;
|
|
3014
3208
|
}
|
|
3209
|
+
async function getTableComment(connector, tableName, schemaName) {
|
|
3210
|
+
try {
|
|
3211
|
+
if (connector.getTableComment) {
|
|
3212
|
+
return await connector.getTableComment(tableName, schemaName);
|
|
3213
|
+
}
|
|
3214
|
+
return null;
|
|
3215
|
+
} catch (error) {
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3015
3219
|
async function searchSchemas(connector, pattern, detailLevel, limit) {
|
|
3016
3220
|
const schemas = await connector.getSchemas();
|
|
3017
3221
|
const regex = likePatternToRegex(pattern);
|
|
@@ -3062,11 +3266,13 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
3062
3266
|
try {
|
|
3063
3267
|
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3064
3268
|
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
3269
|
+
const comment = await getTableComment(connector, tableName, schemaName);
|
|
3065
3270
|
results.push({
|
|
3066
3271
|
name: tableName,
|
|
3067
3272
|
schema: schemaName,
|
|
3068
3273
|
column_count: columns.length,
|
|
3069
|
-
row_count: rowCount
|
|
3274
|
+
row_count: rowCount,
|
|
3275
|
+
...comment ? { comment } : {}
|
|
3070
3276
|
});
|
|
3071
3277
|
} catch (error) {
|
|
3072
3278
|
results.push({
|
|
@@ -3081,16 +3287,19 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
3081
3287
|
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3082
3288
|
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
3083
3289
|
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
3290
|
+
const comment = await getTableComment(connector, tableName, schemaName);
|
|
3084
3291
|
results.push({
|
|
3085
3292
|
name: tableName,
|
|
3086
3293
|
schema: schemaName,
|
|
3087
3294
|
column_count: columns.length,
|
|
3088
3295
|
row_count: rowCount,
|
|
3296
|
+
...comment ? { comment } : {},
|
|
3089
3297
|
columns: columns.map((col) => ({
|
|
3090
3298
|
name: col.column_name,
|
|
3091
3299
|
type: col.data_type,
|
|
3092
3300
|
nullable: col.is_nullable === "YES",
|
|
3093
|
-
default: col.column_default
|
|
3301
|
+
default: col.column_default,
|
|
3302
|
+
...col.description ? { description: col.description } : {}
|
|
3094
3303
|
})),
|
|
3095
3304
|
indexes: indexes.map((idx) => ({
|
|
3096
3305
|
name: idx.index_name,
|
|
@@ -3152,7 +3361,8 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
3152
3361
|
schema: schemaName,
|
|
3153
3362
|
type: column.data_type,
|
|
3154
3363
|
nullable: column.is_nullable === "YES",
|
|
3155
|
-
default: column.column_default
|
|
3364
|
+
default: column.column_default,
|
|
3365
|
+
...column.description ? { description: column.description } : {}
|
|
3156
3366
|
});
|
|
3157
3367
|
}
|
|
3158
3368
|
}
|
|
@@ -3166,7 +3376,7 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
3166
3376
|
}
|
|
3167
3377
|
return results;
|
|
3168
3378
|
}
|
|
3169
|
-
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
3379
|
+
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit, routineType) {
|
|
3170
3380
|
const regex = likePatternToRegex(pattern);
|
|
3171
3381
|
const results = [];
|
|
3172
3382
|
let schemasToSearch;
|
|
@@ -3178,7 +3388,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
|
|
|
3178
3388
|
for (const schemaName of schemasToSearch) {
|
|
3179
3389
|
if (results.length >= limit) break;
|
|
3180
3390
|
try {
|
|
3181
|
-
const procedures = await connector.getStoredProcedures(schemaName);
|
|
3391
|
+
const procedures = await connector.getStoredProcedures(schemaName, routineType);
|
|
3182
3392
|
const matched = procedures.filter((proc) => regex.test(proc));
|
|
3183
3393
|
for (const procName of matched) {
|
|
3184
3394
|
if (results.length >= limit) break;
|
|
@@ -3315,7 +3525,10 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
3315
3525
|
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
3316
3526
|
break;
|
|
3317
3527
|
case "procedure":
|
|
3318
|
-
results = await searchProcedures(connector, pattern, schema, detail_level, limit);
|
|
3528
|
+
results = await searchProcedures(connector, pattern, schema, detail_level, limit, "procedure");
|
|
3529
|
+
break;
|
|
3530
|
+
case "function":
|
|
3531
|
+
results = await searchProcedures(connector, pattern, schema, detail_level, limit, "function");
|
|
3319
3532
|
break;
|
|
3320
3533
|
case "index":
|
|
3321
3534
|
results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
|
|
@@ -3796,9 +4009,9 @@ function generateCode(request) {
|
|
|
3796
4009
|
|
|
3797
4010
|
// src/tools/generate-code-handler.ts
|
|
3798
4011
|
function createGenerateCodeToolHandler() {
|
|
3799
|
-
return async (
|
|
4012
|
+
return async (args, extra) => {
|
|
3800
4013
|
try {
|
|
3801
|
-
const params = generateCodeSchema.parse(
|
|
4014
|
+
const params = generateCodeSchema.parse(args);
|
|
3802
4015
|
const result = generateCode(params);
|
|
3803
4016
|
return {
|
|
3804
4017
|
content: [
|
|
@@ -3975,7 +4188,7 @@ function getSearchObjectsMetadata(sourceId) {
|
|
|
3975
4188
|
const isSingleSource = sourceIds.length === 1;
|
|
3976
4189
|
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
3977
4190
|
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
3978
|
-
const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database`;
|
|
4191
|
+
const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
|
|
3979
4192
|
return {
|
|
3980
4193
|
name: toolName,
|
|
3981
4194
|
description,
|
|
@@ -4655,7 +4868,7 @@ See documentation for more details on configuring database connections.
|
|
|
4655
4868
|
const sources = sourceConfigsData.sources;
|
|
4656
4869
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
4657
4870
|
await connectorManager.connectWithSources(sources);
|
|
4658
|
-
const { initializeToolRegistry } = await import("./registry-
|
|
4871
|
+
const { initializeToolRegistry } = await import("./registry-TGSADHCH.js");
|
|
4659
4872
|
initializeToolRegistry({
|
|
4660
4873
|
sources: sourceConfigsData.sources,
|
|
4661
4874
|
tools: sourceConfigsData.tools
|
|
@@ -4693,9 +4906,6 @@ See documentation for more details on configuring database connections.
|
|
|
4693
4906
|
app.use(express.json());
|
|
4694
4907
|
app.use((req, res, next) => {
|
|
4695
4908
|
const origin = req.headers.origin;
|
|
4696
|
-
if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("https://localhost")) {
|
|
4697
|
-
return res.status(403).json({ error: "Forbidden origin" });
|
|
4698
|
-
}
|
|
4699
4909
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
4700
4910
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4701
4911
|
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teckedd-code2save/datafy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"mcpName": "io.github.teckedd-code2save/datafy",
|
|
5
5
|
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB, Redis, and Elasticsearch with code generation",
|
|
6
6
|
"repository": {
|
|
@@ -17,21 +17,6 @@
|
|
|
17
17
|
"LICENSE",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "pnpm run generate:api-types && tsup && cd frontend && pnpm run build",
|
|
22
|
-
"build:backend": "pnpm run generate:api-types && tsup",
|
|
23
|
-
"build:frontend": "cd frontend && pnpm run build",
|
|
24
|
-
"start": "node dist/index.js",
|
|
25
|
-
"dev": "concurrently --kill-others \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
|
|
26
|
-
"dev:backend": "NODE_ENV=development tsx src/index.ts --transport=http",
|
|
27
|
-
"dev:frontend": "cd frontend && pnpm run dev",
|
|
28
|
-
"crossdev": "cross-env NODE_ENV=development tsx src/index.ts",
|
|
29
|
-
"generate:api-types": "openapi-typescript src/api/openapi.yaml -o src/api/openapi.d.ts",
|
|
30
|
-
"test": "vitest run",
|
|
31
|
-
"test:unit": "vitest run --project unit",
|
|
32
|
-
"test:watch": "vitest",
|
|
33
|
-
"test:integration": "vitest run --project integration"
|
|
34
|
-
},
|
|
35
20
|
"keywords": [],
|
|
36
21
|
"author": "",
|
|
37
22
|
"license": "MIT",
|
|
@@ -86,5 +71,19 @@
|
|
|
86
71
|
"include": [
|
|
87
72
|
"src/**/*"
|
|
88
73
|
],
|
|
89
|
-
"
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "pnpm run generate:api-types && tsup && cd frontend && pnpm run build",
|
|
76
|
+
"build:backend": "pnpm run generate:api-types && tsup",
|
|
77
|
+
"build:frontend": "cd frontend && pnpm run build",
|
|
78
|
+
"start": "node dist/index.js",
|
|
79
|
+
"dev": "concurrently --kill-others \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
|
|
80
|
+
"dev:backend": "NODE_ENV=development tsx src/index.ts --transport=http",
|
|
81
|
+
"dev:frontend": "cd frontend && pnpm run dev",
|
|
82
|
+
"crossdev": "cross-env NODE_ENV=development tsx src/index.ts",
|
|
83
|
+
"generate:api-types": "openapi-typescript src/api/openapi.yaml -o src/api/openapi.d.ts",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:unit": "vitest run --project unit",
|
|
86
|
+
"test:watch": "vitest",
|
|
87
|
+
"test:integration": "vitest run --project integration"
|
|
88
|
+
}
|
|
90
89
|
}
|