Skip to content

Commit 670f006

Browse files
authored
Merge pull request #264 from constructive-io/devin/1767670912-fix-equals-assignment
fix(plpgsql-deparser): handle = assignment operator in splitAssignment
2 parents 3b3beb1 + cbbc4ac commit 670f006

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

packages/plpgsql-deparser/__tests__/hydrate.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ $$`;
2424
expect(result.stats.parsedExpressions).toBeGreaterThan(0);
2525
});
2626

27-
it('should hydrate assignment expressions', () => {
27+
it('should hydrate assignment expressions with :=', () => {
2828
const sql = `CREATE FUNCTION test_func() RETURNS integer
2929
LANGUAGE plpgsql
3030
AS $$
@@ -50,6 +50,36 @@ $$`;
5050
}
5151
});
5252

53+
it('should hydrate assignment expressions with = (not just :=)', () => {
54+
// PL/pgSQL allows both := and = for assignments
55+
// This test ensures = assignments are properly hydrated with valueExpr
56+
const sql = `CREATE FUNCTION test_func() RETURNS void
57+
LANGUAGE plpgsql
58+
AS $$
59+
DECLARE
60+
x text;
61+
BEGIN
62+
x = "constructive-simple-secrets".get('a', 'b');
63+
END;
64+
$$`;
65+
66+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
67+
const result = hydratePlpgsqlAst(parsed);
68+
69+
expect(result.errors).toHaveLength(0);
70+
expect(result.stats.assignmentExpressions).toBeGreaterThan(0);
71+
72+
const assignExpr = findExprByKind(result.ast, 'assign');
73+
expect(assignExpr).toBeDefined();
74+
if (assignExpr && assignExpr.kind === 'assign') {
75+
expect(assignExpr.target).toBe('x');
76+
expect(assignExpr.value).toContain('constructive-simple-secrets');
77+
// Critical: valueExpr must be populated for AST-based transformations
78+
expect(assignExpr.valueExpr).toBeDefined();
79+
expect(assignExpr.valueExpr).not.toBeNull();
80+
}
81+
});
82+
5383
it('should hydrate IF condition expressions', () => {
5484
const sql = `CREATE FUNCTION test_func(p_val integer) RETURNS text
5585
LANGUAGE plpgsql

packages/plpgsql-deparser/src/hydrate.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ function splitAssignment(query: string): { target: string; value: string } | nul
316316
try {
317317
const tokens = scanSync(query);
318318
let assignIndex = -1;
319+
let assignTokenText = '';
319320
let parenDepth = 0;
320321
let bracketDepth = 0;
321322

@@ -327,8 +328,29 @@ function splitAssignment(query: string): { target: string; value: string } | nul
327328
else if (token.text === '[') bracketDepth++;
328329
else if (token.text === ']') bracketDepth--;
329330

331+
// Check for := first (preferred PL/pgSQL assignment operator)
330332
if (token.text === ':=' && parenDepth === 0 && bracketDepth === 0) {
331333
assignIndex = i;
334+
assignTokenText = ':=';
335+
break;
336+
}
337+
338+
// Also check for = (valid PL/pgSQL assignment operator)
339+
// But avoid => (named parameter syntax) by checking the previous token
340+
if (token.text === '=' && parenDepth === 0 && bracketDepth === 0) {
341+
// Make sure this isn't part of => (named parameter)
342+
// or comparison operators like >=, <=, <>, !=
343+
const prevToken = i > 0 ? tokens.tokens[i - 1] : null;
344+
const prevText = prevToken?.text || '';
345+
346+
// Skip if previous token suggests this is not an assignment
347+
if (prevText === '>' || prevText === '<' || prevText === '!' || prevText === ':') {
348+
continue;
349+
}
350+
351+
// This looks like an assignment with =
352+
assignIndex = i;
353+
assignTokenText = '=';
332354
break;
333355
}
334356
}
@@ -343,13 +365,33 @@ function splitAssignment(query: string): { target: string; value: string } | nul
343365

344366
return { target, value };
345367
} catch (err) {
368+
// Fallback: try to find := first, then =
346369
const colonIndex = query.indexOf(':=');
347-
if (colonIndex === -1) {
348-
return null;
370+
if (colonIndex !== -1) {
371+
const target = query.substring(0, colonIndex).trim();
372+
const value = query.substring(colonIndex + 2).trim();
373+
return { target, value };
349374
}
350-
const target = query.substring(0, colonIndex).trim();
351-
const value = query.substring(colonIndex + 2).trim();
352-
return { target, value };
375+
376+
// Try to find = (but be careful about >=, <=, <>, !=, =>)
377+
// Find the first = that's not part of a comparison operator
378+
for (let i = 0; i < query.length; i++) {
379+
if (query[i] === '=') {
380+
const prev = i > 0 ? query[i - 1] : '';
381+
const next = i < query.length - 1 ? query[i + 1] : '';
382+
383+
// Skip comparison operators
384+
if (prev === '>' || prev === '<' || prev === '!' || prev === ':' || next === '>') {
385+
continue;
386+
}
387+
388+
const target = query.substring(0, i).trim();
389+
const value = query.substring(i + 1).trim();
390+
return { target, value };
391+
}
392+
}
393+
394+
return null;
353395
}
354396
}
355397

0 commit comments

Comments
 (0)