Skip to content

Commit 048caae

Browse files
committed
feat: hoist mid-statement comments above their enclosing statement
Comments like 'SELECT id, -- note' that fall inside a statement's byte range are detected and repositioned above the statement rather than trailing at the end. New fixture mid-statement-comments.sql documents this behavior with snapshots. 68 total tests, 8 snapshots.
1 parent 62a1e45 commit 048caae

3 files changed

Lines changed: 113 additions & 7 deletions

File tree

packages/parse/__tests__/__snapshots__/roundtrip.test.ts.snap

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,46 @@ GRANT SELECT ON app.users TO authenticated;
7575
GRANT ALL ON app.users TO admin_role;"
7676
`;
7777

78+
exports[`fixture round-trip (CST) mid-statement-comments deparsed output matches snapshot 1`] = `
79+
"-- Mid-statement comments are hoisted above their enclosing statement.
80+
-- The deparser cannot inject comments back into the middle of a
81+
-- statement, so they are preserved as standalone lines above it.
82+
83+
-- Simple mid-statement comment
84+
-- the primary key
85+
SELECT
86+
id,
87+
name
88+
FROM users;
89+
90+
-- Multiple mid-statement comments in one query
91+
-- user ID
92+
-- display name
93+
-- role from join
94+
SELECT
95+
u.id,
96+
u.name,
97+
r.role_name
98+
FROM users AS u
99+
JOIN roles AS r ON r.id = u.role_id;
100+
101+
-- Mid-statement comment in INSERT values
102+
-- log level
103+
-- log body
104+
INSERT INTO logs (
105+
level,
106+
message
107+
) VALUES
108+
('info', 'hello');
109+
110+
-- Comment between clauses
111+
-- filter active only
112+
SELECT id
113+
FROM users
114+
WHERE
115+
active = true;"
116+
`;
117+
78118
exports[`fixture round-trip (CST) multi-statement deparsed output matches snapshot 1`] = `
79119
"-- Schema setup
80120
CREATE SCHEMA IF NOT EXISTS app;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- Mid-statement comments are hoisted above their enclosing statement.
2+
-- The deparser cannot inject comments back into the middle of a
3+
-- statement, so they are preserved as standalone lines above it.
4+
5+
-- Simple mid-statement comment
6+
SELECT
7+
id, -- the primary key
8+
name
9+
FROM users;
10+
11+
-- Multiple mid-statement comments in one query
12+
SELECT
13+
u.id, -- user ID
14+
u.name, -- display name
15+
r.role_name -- role from join
16+
FROM users u
17+
JOIN roles r ON r.id = u.role_id;
18+
19+
-- Mid-statement comment in INSERT values
20+
INSERT INTO logs (level, message)
21+
VALUES (
22+
'info', -- log level
23+
'hello' -- log body
24+
);
25+
26+
-- Comment between clauses
27+
SELECT id
28+
FROM users
29+
-- filter active only
30+
WHERE active = true;

packages/parse/src/parse.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,64 @@ function findActualSqlStart(
6666
* because stmt_location can include preceding whitespace/comments,
6767
* making a simple merge unreliable.
6868
*/
69+
/**
70+
* Build a list of statement byte ranges (actualStart … end) so we can
71+
* detect mid-statement comments and hoist them above their enclosing
72+
* statement instead of leaving them trailing at the wrong position.
73+
*/
74+
interface StmtRange {
75+
actualStart: number;
76+
end: number; // stmt_location + stmt_len, or sql.length for last stmt
77+
}
78+
79+
function buildStmtRanges(
80+
stmts: RawStmt[],
81+
sql: string,
82+
elements: ScannedElement[]
83+
): StmtRange[] {
84+
return stmts.map(stmt => {
85+
const loc = stmt.stmt_location ?? 0;
86+
const actualStart = findActualSqlStart(sql, loc, elements);
87+
const len = stmt.stmt_len ?? sql.length - loc;
88+
return { actualStart, end: loc + len };
89+
});
90+
}
91+
6992
function interleave(
7093
parseResult: ParseResult,
7194
sql: string,
7295
elements: ScannedElement[]
7396
): EnhancedParseResult {
7497
const stmts = parseResult.stmts ?? [];
7598
const items: SortableEntry[] = [];
99+
const ranges = buildStmtRanges(stmts, sql, elements);
76100

77101
// Add scanned elements (comments and whitespace)
78102
for (const elem of elements) {
79103
if (elem.kind === 'comment') {
104+
// Check if this comment falls inside a statement's byte range.
105+
// If so, hoist it above that statement instead of leaving it
106+
// at its original position (which would place it after the
107+
// statement or trailing at the wrong spot).
108+
let hoistedPosition: number | null = null;
109+
for (const range of ranges) {
110+
if (elem.value.start > range.actualStart && elem.value.start < range.end) {
111+
hoistedPosition = range.actualStart;
112+
break;
113+
}
114+
}
115+
80116
items.push({
81-
position: elem.value.start,
117+
position: hoistedPosition ?? elem.value.start,
82118
priority: 0,
83119
entry: {
84120
RawComment: {
85121
type: elem.value.type,
86122
text: elem.value.text,
87123
location: elem.value.start,
88-
...(elem.value.trailing ? { trailing: true } : {}),
124+
// Only preserve trailing flag when NOT hoisted —
125+
// a hoisted comment becomes a standalone line above the statement.
126+
...(hoistedPosition == null && elem.value.trailing ? { trailing: true } : {}),
89127
}
90128
}
91129
});
@@ -104,13 +142,11 @@ function interleave(
104142
}
105143

106144
// Add parsed statements with their actual SQL start position
107-
for (const stmt of stmts) {
108-
const loc = stmt.stmt_location ?? 0;
109-
const actualStart = findActualSqlStart(sql, loc, elements);
145+
for (let i = 0; i < stmts.length; i++) {
110146
items.push({
111-
position: actualStart,
147+
position: ranges[i].actualStart,
112148
priority: 2, // statements sort after comments and whitespace
113-
entry: stmt,
149+
entry: stmts[i],
114150
});
115151
}
116152

0 commit comments

Comments
 (0)