Skip to content

Commit 186d174

Browse files
akoclaude
andcommitted
Fix XPath token quoting for [%CurrentDateTime%] (GitHub issue #1)
Mendix tokens like [%CurrentDateTime%] must be single-quoted in XPath constraints but not in Mendix expressions. Added expressionToXPath() that quotes TokenExpr nodes for XPath context, with unit and mx check integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9f9484d commit 186d174

5 files changed

Lines changed: 146 additions & 1 deletion

File tree

mdl-examples/doctype-tests/02-microflow-examples.mdl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,21 @@ BEGIN
880880
END;
881881
/
882882

883+
/**
884+
* Example 7.5: RETRIEVE with [%CurrentDateTime%] token in WHERE
885+
*
886+
* XPath tokens like [%CurrentDateTime%] must be quoted in XPath constraints.
887+
* Tests fix for GitHub issue #1.
888+
*/
889+
CREATE MICROFLOW MfTest.M028_RetrieveWithDateTimeToken ()
890+
RETURNS List of MfTest.Order AS $OverdueOrders
891+
BEGIN
892+
RETRIEVE $OverdueOrders FROM MfTest.Order
893+
WHERE ShipDate < [%CurrentDateTime%];
894+
RETURN $OverdueOrders;
895+
END;
896+
/
897+
883898
-- MARK: Real World
884899

885900
-- ============================================================================

mdl/executor/bugfix_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,59 @@ func TestEnumDefaultNotDoubleQualified(t *testing.T) {
337337
}
338338
}
339339

340+
// TestExpressionToXPath_TokenQuoting verifies that [%CurrentDateTime%] tokens
341+
// are quoted in XPath context but not in Mendix expression context (GitHub issue #1).
342+
func TestExpressionToXPath_TokenQuoting(t *testing.T) {
343+
tests := []struct {
344+
name string
345+
expr ast.Expression
346+
wantExpr string // expressionToString output
347+
wantXP string // expressionToXPath output
348+
}{
349+
{
350+
name: "Token_CurrentDateTime",
351+
expr: &ast.TokenExpr{Token: "CurrentDateTime"},
352+
wantExpr: "[%CurrentDateTime%]",
353+
wantXP: "'[%CurrentDateTime%]'",
354+
},
355+
{
356+
name: "Token_CurrentUser",
357+
expr: &ast.TokenExpr{Token: "CurrentUser"},
358+
wantExpr: "[%CurrentUser%]",
359+
wantXP: "'[%CurrentUser%]'",
360+
},
361+
{
362+
name: "BinaryExpr_with_token",
363+
expr: &ast.BinaryExpr{
364+
Left: &ast.IdentifierExpr{Name: "DueDate"},
365+
Operator: "<",
366+
Right: &ast.TokenExpr{Token: "CurrentDateTime"},
367+
},
368+
wantExpr: "DueDate < [%CurrentDateTime%]",
369+
wantXP: "DueDate < '[%CurrentDateTime%]'",
370+
},
371+
{
372+
name: "Variable_unchanged",
373+
expr: &ast.VariableExpr{Name: "MyVar"},
374+
wantExpr: "$MyVar",
375+
wantXP: "$MyVar",
376+
},
377+
}
378+
379+
for _, tt := range tests {
380+
t.Run(tt.name, func(t *testing.T) {
381+
gotExpr := expressionToString(tt.expr)
382+
if gotExpr != tt.wantExpr {
383+
t.Errorf("expressionToString() = %q, want %q", gotExpr, tt.wantExpr)
384+
}
385+
gotXP := expressionToXPath(tt.expr)
386+
if gotXP != tt.wantXP {
387+
t.Errorf("expressionToXPath() = %q, want %q", gotXP, tt.wantXP)
388+
}
389+
})
390+
}
391+
}
392+
340393
// validateMicroflowFromMDL parses a CREATE MICROFLOW statement and runs
341394
// ValidateMicroflowBody, returning any validation errors.
342395
func validateMicroflowFromMDL(t *testing.T, input string) []string {

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
312312
// Convert WHERE expression if present
313313
// XPath constraints are stored with square brackets in BSON: [expression]
314314
if s.Where != nil {
315-
source.XPathConstraint = "[" + expressionToString(s.Where) + "]"
315+
source.XPathConstraint = "[" + expressionToXPath(s.Where) + "]"
316316
}
317317

318318
// Convert SORT BY columns if present

mdl/executor/cmd_microflows_helpers.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,37 @@ func expressionToString(expr ast.Expression) string {
132132
}
133133
}
134134

135+
// expressionToXPath converts an AST Expression to an XPath constraint string.
136+
// Unlike expressionToString (for Mendix expressions), XPath requires Mendix
137+
// tokens like [%CurrentDateTime%] to be quoted: '[%CurrentDateTime%]'.
138+
func expressionToXPath(expr ast.Expression) string {
139+
if expr == nil {
140+
return ""
141+
}
142+
if reflect.ValueOf(expr).IsNil() {
143+
return ""
144+
}
145+
146+
switch e := expr.(type) {
147+
case *ast.TokenExpr:
148+
return "'[%" + e.Token + "%]'"
149+
case *ast.BinaryExpr:
150+
left := expressionToXPath(e.Left)
151+
right := expressionToXPath(e.Right)
152+
op := strings.ToLower(e.Operator)
153+
return left + " " + op + " " + right
154+
case *ast.UnaryExpr:
155+
operand := expressionToXPath(e.Operand)
156+
op := strings.ToLower(e.Operator)
157+
return op + " " + operand
158+
case *ast.ParenExpr:
159+
return "(" + expressionToXPath(e.Inner) + ")"
160+
default:
161+
// For all other expression types, the standard serialization is correct
162+
return expressionToString(expr)
163+
}
164+
}
165+
135166
// countMicroflowActivities counts the number of meaningful activities in a microflow.
136167
// Excludes structural elements like StartEvent, EndEvent, and merge nodes.
137168
func countMicroflowActivities(mf *microflows.Microflow) int {

mdl/executor/roundtrip_mxcheck_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,52 @@ func TestMxCheck_CE0066_Scenarios(t *testing.T) {
448448
}
449449
}
450450

451+
// TestMxCheck_RetrieveWithDateTimeToken validates that RETRIEVE with [%CurrentDateTime%]
452+
// in a WHERE clause produces correctly quoted XPath (GitHub issue #1).
453+
// The token must be quoted as '[%CurrentDateTime%]' in XPath constraints.
454+
func TestMxCheck_RetrieveWithDateTimeToken(t *testing.T) {
455+
if !mxCheckAvailable() {
456+
t.Skip("mx command not available")
457+
}
458+
459+
env := setupTestEnv(t)
460+
defer env.teardown()
461+
462+
if err := env.executeMDL(`CREATE OR MODIFY PERSISTENT ENTITY RoundtripTest.MxCheckDated (
463+
Name: String(100),
464+
DueDate: DateTime
465+
);`); err != nil {
466+
t.Fatalf("Failed to create entity: %v", err)
467+
}
468+
469+
mfName := testModule + ".MxCheck_DateTimeToken"
470+
env.registerCleanup("microflow", mfName)
471+
472+
createMDL := `CREATE MICROFLOW ` + mfName + ` () RETURNS Boolean
473+
BEGIN
474+
RETRIEVE $Items FROM RoundtripTest.MxCheckDated
475+
WHERE DueDate < [%CurrentDateTime%];
476+
RETURN true;
477+
END;`
478+
479+
if err := env.executeMDL(createMDL); err != nil {
480+
t.Fatalf("Failed to create microflow: %v", err)
481+
}
482+
483+
env.executor.Execute(&ast.DisconnectStmt{})
484+
485+
output, err := runMxCheck(t, env.projectPath)
486+
if err != nil {
487+
if strings.Contains(output, "error") || strings.Contains(output, "Error") {
488+
t.Errorf("mx check found errors:\n%s", output)
489+
} else {
490+
t.Logf("mx check output:\n%s", output)
491+
}
492+
} else {
493+
t.Logf("mx check passed:\n%s", output)
494+
}
495+
}
496+
451497
// TestMxCheck_MicroflowWithCallParams tests microflow with CALL unified param syntax.
452498
func TestMxCheck_MicroflowWithCallParams(t *testing.T) {
453499
if !mxCheckAvailable() {

0 commit comments

Comments
 (0)