Skip to content

Commit c78bfd5

Browse files
akoclaude
andcommitted
fix: hint at correct attribute-level GRANT syntax in parse errors
When users write `GRANT ... (READ "Attr1", "Attr2")` with quoted strings instead of `READ (Attr1, Attr2)`, the parse error now points at the correct syntax. Fixes issue #203 where AI agents silently fell back to `READ *` rather than using fine-grained attribute access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3575319 commit c78bfd5

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

mdl/visitor/visitor.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ func (l *errorListener) SyntaxError(_ antlr.Recognizer, _ any, line, column int,
3535
// enhanceErrorMessage checks if an error message indicates a reserved keyword
3636
// was used as an identifier and provides a more helpful message.
3737
func enhanceErrorMessage(msg string) string {
38+
// Check for quoted attribute names after READ/WRITE in a GRANT clause.
39+
// Users often write `READ "Attr1", "Attr2"` instead of the correct
40+
// `READ (Attr1, Attr2)` — the grammar expects unquoted identifiers in parens.
41+
if looksLikeQuotedGrantAttribute(msg) {
42+
return fmt.Sprintf("%s\n\n Attribute-level GRANT uses unquoted identifiers inside parentheses,\n"+
43+
" not quoted strings. Comma-separate multiple attributes:\n"+
44+
" GRANT Mod.Role ON Mod.Entity (READ (Attr1, Attr2), WRITE (Attr1)); (correct)\n"+
45+
" GRANT Mod.Role ON Mod.Entity (READ \"Attr1\", \"Attr2\"); (wrong — causes parse error)", msg)
46+
}
47+
3848
// Check for unescaped apostrophe in string literals first.
3949
// When 'it's here' is parsed, ANTLR sees 'it' as a complete string, then
4050
// the leftover characters (like "s", "ll", "t") appear as unexpected tokens.
@@ -86,6 +96,26 @@ func enhanceErrorMessage(msg string) string {
8696
return msg
8797
}
8898

99+
// looksLikeQuotedGrantAttribute detects ANTLR errors from `READ "Attr"` /
100+
// `WRITE "Attr"` — a common mistake where users quote attribute names instead
101+
// of using the correct `READ (Attr1, Attr2)` identifier list.
102+
//
103+
// Typical ANTLR shapes:
104+
// - no viable alternative at input 'READ"Attr1"'
105+
// - no viable alternative at input 'WRITE"Attr1"'
106+
// - mismatched input '"Attr"' expecting {CREATE, DELETE, READ, WRITE}
107+
func looksLikeQuotedGrantAttribute(msg string) bool {
108+
if strings.Contains(msg, `input 'READ"`) || strings.Contains(msg, `input 'WRITE"`) {
109+
return true
110+
}
111+
// Quoted string appearing where a GRANT access right is expected.
112+
if strings.Contains(msg, `expecting {CREATE, DELETE, READ, WRITE}`) &&
113+
(strings.Contains(msg, `input '"`) || strings.Contains(msg, `input "`)) {
114+
return true
115+
}
116+
return false
117+
}
118+
89119
// looksLikeUnescapedApostrophe detects ANTLR errors that are likely caused by
90120
// unescaped apostrophes in string literals. When 'don't' is parsed, ANTLR sees
91121
// 'don' as a complete string, then 't' as an unexpected token, producing errors

mdl/visitor/visitor_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,65 @@ END;`
11111111
}
11121112
}
11131113

1114+
func TestEnhanceErrorMessage_QuotedGrantAttribute(t *testing.T) {
1115+
tests := []struct {
1116+
name string
1117+
msg string
1118+
wantHint bool
1119+
}{
1120+
{
1121+
name: "READ with quoted attribute",
1122+
msg: `line 1:51 no viable alternative at input 'READ"Attr1"'`,
1123+
wantHint: true,
1124+
},
1125+
{
1126+
name: "WRITE with quoted attribute",
1127+
msg: `line 1:75 no viable alternative at input 'WRITE"Attr1"'`,
1128+
wantHint: true,
1129+
},
1130+
{
1131+
name: "quoted string where access right expected",
1132+
msg: `mismatched input '"Attr2"' expecting {CREATE, DELETE, READ, WRITE}`,
1133+
wantHint: true,
1134+
},
1135+
{
1136+
name: "unrelated error does not trigger",
1137+
msg: `no viable alternative at input 'CREATE PERSISTENT'`,
1138+
wantHint: false,
1139+
},
1140+
}
1141+
for _, tt := range tests {
1142+
t.Run(tt.name, func(t *testing.T) {
1143+
result := enhanceErrorMessage(tt.msg)
1144+
hasHint := strings.Contains(result, "Attribute-level GRANT")
1145+
if hasHint != tt.wantHint {
1146+
t.Errorf("expected hint=%v\n input: %s\n output: %s", tt.wantHint, tt.msg, result)
1147+
}
1148+
})
1149+
}
1150+
}
1151+
1152+
func TestParseError_QuotedGrantAttribute(t *testing.T) {
1153+
input := `GRANT Mod.Role ON Mod.Entity (READ "Attr1", "Attr2");`
1154+
_, errs := Build(input)
1155+
if len(errs) == 0 {
1156+
t.Fatal("expected parse errors for quoted GRANT attribute")
1157+
}
1158+
found := false
1159+
for _, err := range errs {
1160+
if strings.Contains(err.Error(), "Attribute-level GRANT") {
1161+
found = true
1162+
break
1163+
}
1164+
}
1165+
if !found {
1166+
t.Errorf("expected GRANT-attribute hint in error messages, got:\n")
1167+
for _, err := range errs {
1168+
t.Errorf(" %v", err)
1169+
}
1170+
}
1171+
}
1172+
11141173
// TestEnumDefaultQuotedIdentifier verifies that quoted identifiers in enum
11151174
// DEFAULT values are unquoted correctly (issue #11 / BUG-004).
11161175
// e.g. DEFAULT MaisonElegance."FormSubmissionStatus".StatusNew should store

0 commit comments

Comments
 (0)