Use forked node-sql-parser, fix CTE issues in parsed SQL (#38744)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34635

# Details

This PR switches us to a [fork of
node-sql-parser](https://github.com/sgress454/node-sql-parser) that I'm
maintaining to fast-track fixes to the SQLite implementation. The first
published version of the fork is 5.4.0-fork.1 (forked from v5.4.0 of the
upstream), and includes fixes for #34635 and #30109 that haven't made it
to the upstream yet.

Fixes in 5.4.0-fork.1:

* https://github.com/sgress454/node-sql-parser/pull/7
* https://github.com/sgress454/node-sql-parser/pull/5
* https://github.com/sgress454/node-sql-parser/pull/4

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [X] Added/updated automated tests
- Granular tests are added [in the package
itself](https://github.com/sgress454/node-sql-parser/blob/5.4.0-fork.1/test/sqlite.spec.js),
and new regression tests for the Fleet issues are added in the Fleet.
- [X] QA'd all new/changed functionality manually
 - Pasted the offending queries into the editor and saw no syntax errors
This commit is contained in:
Scott Gress
2026-01-28 16:08:33 -06:00
committed by GitHub
parent 155b6b1c4e
commit efe266b026
10 changed files with 101 additions and 8 deletions
@@ -1,5 +1,4 @@
// @ts-ignore
import { Parser } from "utilities/node-sql-parser/sqlite";
import { Parser } from "node-sql-parser";
export const EMPTY_QUERY_ERR = "Query text must be present";
export const INVALID_SYNTAX_ERR = "Syntax error. Please review before saving.";
@@ -43,3 +43,73 @@ describe("validateQuery", () => {
});
});
});
describe("node-sql-parser integration", () => {
it("#30109 - allow custom escape characters in LIKE clauses", () => {
const query = `
WITH localusers AS (
SELECT username, directory || '/.gitconfig' AS gc_path
FROM users
WHERE
shell != '/usr/bin/false'
AND username NOT LIKE '\\_%' ESCAPE '\\'
AND username NOT IN ('root', 'person1', 'person2', 'SYSTEM', 'LOCAL SERVICE', 'NETWORK SERVICE')
AND directory != ''
)
SELECT username, value AS git_signingkey_path
FROM parse_ini
LEFT JOIN localusers ON parse_ini.path=localusers.gc_path
WHERE path IN (SELECT gc_path FROM localusers) AND fullkey = 'user/signingkey';
`;
const { error, valid } = validateQuery(query);
expect(valid).toEqual(true);
expect(error).toBeFalsy();
});
it("#34635 - allow VALUES in CTEs, and table names in IN clauses", () => {
const query = `
-- Step 1: Define config file path suffixes for each supported application
WITH path_suffixes(path) AS (
VALUES
('/.cursor/mcp.json'), -- Cursor, macOS/Linux/Windows
('/Library/Application Support/Claude/claude_desktop_config.json'), -- Claude Desktop, macOS
('\\AppData\\Roaming\\Claude\\claude_desktop_config.json'), -- Claude Desktop, Windows
('/.claude.json'), -- Claude Code, macOS/Linux
('/Library/Application Support/Code/User/mcp.json'), -- VSCode, macOS
('/.config/Code/User/mcp.json'), -- VSCode, Linux
('\\AppData\\Roaming\\Code\\User\\mcp.json'), -- VSCode, Windows
('/.codeium/windsurf/mcp_config.json'), -- Windsurf, macOS
('/.gemini/settings.json'), -- Gemini CLI, macOS/Linux/Windows
('/.lmstudio/mcp.json') -- LMStudio, macOS/Linux/Windows
),
-- Step 2: Build full file paths by combining each user's home directory with the path suffixes
full_paths AS (
SELECT directory || path AS full_path
FROM users
JOIN path_suffixes
),
-- Step 3: Read config files that exist and concatenate their lines into complete JSON strings
config_files AS (
SELECT path, group_concat(line, '') AS contents
FROM file_lines
WHERE path IN full_paths
GROUP BY path
)
-- Step 4: Parse JSON and extract each MCP server configuration
SELECT
config_files.path,
key AS name,
value AS mcp_config
FROM config_files
JOIN json_each(
COALESCE(
config_files.contents->'$.mcpServers',
config_files.contents->'$.servers'
) -- Most configs use 'mcpServers' key, but some use 'servers' key
)
`;
const { error, valid } = validateQuery(query);
expect(valid).toEqual(true);
expect(error).toBeFalsy();
});
});