From afed1fcb55fbf3ce143b642ffe1341af4b3d3deb Mon Sep 17 00:00:00 2001 From: delchev Date: Mon, 20 Jul 2026 13:38:39 +0300 Subject: [PATCH] fix(intent): report filter translates bare to-one relation names to their FK columns A report filter like `Status != 8` (a to-one RELATION of the source, not a field) passed through buildWhere untranslated - fields rewrote to their qualified physical columns but relation names did not, so the generated query carried a nonexistent column and failed at SQL time. Bare to-one relation names now rewrite to the FK column (Invoice."INVOICE_STATUS"), with guards so join-alias tokens from the dotted-ref pass (Customer."CUSTOMER_NAME") and already-quoted columns are left intact. Found by a suite emission audit: an overdue-invoices report excluding a VOIDED status generated 'AND Status != 8' verbatim into the WHERE. Verified: ReportIntentGeneratorTest +1 (bare relation translated, dotted-ref alias unmangled), full class green. Co-Authored-By: Claude Fable 5 --- .../components/dirigible-java-script.json | 2 +- .../report/ReportIntentGenerator.java | 13 ++++++ .../report/ReportIntentGeneratorTest.java | 43 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/components/engine/engine-camel/src/generated/resources/META-INF/org/eclipse/dirigible/components/engine/camel/components/dirigible-java-script.json b/components/engine/engine-camel/src/generated/resources/META-INF/org/eclipse/dirigible/components/engine/camel/components/dirigible-java-script.json index de407a13194..f9a8da5dc24 100644 --- a/components/engine/engine-camel/src/generated/resources/META-INF/org/eclipse/dirigible/components/engine/camel/components/dirigible-java-script.json +++ b/components/engine/engine-camel/src/generated/resources/META-INF/org/eclipse/dirigible/components/engine/camel/components/dirigible-java-script.json @@ -11,7 +11,7 @@ "supportLevel": "Stable", "groupId": "org.eclipse.dirigible", "artifactId": "dirigible-components-engine-camel", - "version": "14.0.0-SNAPSHOT", + "version": "15.0.0-SNAPSHOT", "scheme": "dirigible-java-script", "extendsScheme": "", "syntax": "dirigible-java-script:javaScriptPath", diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGenerator.java index 457ffaf7686..fa6c944cdd2 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGenerator.java @@ -610,6 +610,19 @@ private static String buildWhere(IntentGenerationContext context, IntentModel mo Matcher.quoteReplacement(baseAlias + "." + quote(column(source.getName(), field.getName())))); } } + // A bare to-one RELATION name filters by its FK column (`Status != 8` -> the status FK + // id column) - previously it passed through untranslated and broke the generated SQL. + // The negative lookahead skips join-alias usages (`Customer."CUSTOMER_NAME"` from the + // dotted-ref pass above); the lookbehind skips already-qualified column tokens. + if (source.getRelations() != null) { + for (RelationIntent relation : source.getRelations()) { + if (relation.getName() != null && !relation.getName() + .isBlank()) { + where = where.replaceAll("(?=`/`!=` are untouched. // Normalize only OUTSIDE single-quoted string literals so a value literal that itself contains diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGeneratorTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGeneratorTest.java index aafda3f5a1b..a76f6bb0352 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGeneratorTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGeneratorTest.java @@ -192,6 +192,49 @@ void listWidgetCarriesTheLimitAndLiteralPinsKeepTheirValue() { .get("token")); } + private static final String STATUS_FILTER_INTENT = """ + name: billing + entities: + - name: InvoiceStatus + kind: setting + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: name, type: string } + - name: Customer + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: name, type: string } + - name: Invoice + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: number, type: string } + - { name: due, type: date } + - { name: balance, type: decimal } + relations: + - { name: Status, kind: manyToOne, to: InvoiceStatus, function: EntityStatus, init: 1 } + - { name: Customer, kind: manyToOne, to: Customer } + reports: + - name: OverdueInvoices + source: Invoice + dimensions: [number, due, Customer.name] + filter: "due <= CURRENT_DATE AND Customer.name != 'X' AND Status != 8" + """; + + @Test + void filterTranslatesABareToOneRelationToItsFkColumn() { + IntentModel model = IntentParser.parse(STATUS_FILTER_INTENT); + Map document = ReportIntentGenerator.buildForTest(TestContexts.context(model), model.getReports() + .get(0)); + String query = (String) document.get("query"); + // A bare to-one relation name filters by its FK column - previously it passed through + // untranslated (`AND Status != 8`) and broke the generated SQL. + assertTrue(query.contains("Invoice.\"INVOICE_STATUS\" != 8"), query); + // The dotted ref keeps its join-alias form - the bare-relation pass must not mangle the + // alias token it produced. + assertTrue(query.contains("Customer.\"CUSTOMER_NAME\" != 'X'"), query); + assertTrue(!query.contains(" Status "), query); + } + private static final String LEDGER_INTENT = """ name: ledger entities: