Compare commits

..
1 Commits
Author SHA1 Message Date
Pijus KamandulisandCursor Agent c78842726b Support single-quoted string literals in query parsing (#18)
* Support single-quoted string literals in query parsing

Add single-quoted string literal support to the NoSQL query parser,
matching Cosmos DB behavior where apostrophes are escaped by doubling
them (''). Includes parser unit tests and an API integration test.

Co-authored-by: Pijus Kamandulis <[email protected]>

* Use backslash escape for apostrophes in single-quoted strings

Cosmos DB escapes apostrophes with \' rather than SQL-style doubling.
Also fix EscapeCharacter to return proper values for \', \", and \\
which were previously producing empty substitutions.

Co-authored-by: Pijus Kamandulis <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
2026-07-12 14:02:23 +03:00
4 changed files with 1570 additions and 1338 deletions
+28
View File
@@ -134,6 +134,34 @@ func Test_Documents(t *testing.T) {
) )
}) })
t.Run("Should query document with single-quoted string literal", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`select c.id
FROM c
WHERE c.id='67890'
ORDER BY c.id`,
nil,
[]interface{}{
map[string]interface{}{"id": "67890"},
},
)
})
t.Run("Should query document with escaped single-quoted string literal", func(t *testing.T) {
ts.DataStore.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "apostrophe-doc", "pk": "ap", "name": "it's"})
testCosmosQuery(t, collectionClient,
`select c.id
FROM c
WHERE c.name='it\'s'
ORDER BY c.id`,
nil,
[]interface{}{
map[string]interface{}{"id": "apostrophe-doc"},
},
)
})
t.Run("Should query document with query parameters", func(t *testing.T) { t.Run("Should query document with query parameters", func(t *testing.T) {
testCosmosQuery(t, collectionClient, testCosmosQuery(t, collectionClient,
`select c.id `select c.id
+1487 -1314
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -509,6 +509,8 @@ IntegerLiteral <- number:Integer {
} }
StringLiteral <- "\"" chars:StringCharacter* "\"" { StringLiteral <- "\"" chars:StringCharacter* "\"" {
return parsers.Constant{Type: parsers.ConstantTypeString,Value: joinStrings(chars.([]interface{}))}, nil return parsers.Constant{Type: parsers.ConstantTypeString,Value: joinStrings(chars.([]interface{}))}, nil
} / "'" chars:SingleQuotedStringCharacter* "'" {
return parsers.Constant{Type: parsers.ConstantTypeString,Value: joinStrings(chars.([]interface{}))}, nil
} }
FloatLiteral <- [0-9]+"."[0-9]+ { FloatLiteral <- [0-9]+"."[0-9]+ {
floatValue, _ := strconv.ParseFloat(string(c.text), 64) floatValue, _ := strconv.ParseFloat(string(c.text), 64)
@@ -869,11 +871,14 @@ Integer <- [0-9]+ {
StringCharacter <- !('"' / "\\") . { return string(c.text), nil } StringCharacter <- !('"' / "\\") . { return string(c.text), nil }
/ "\\" seq:EscapeSequenceCharacter { return seq, nil } / "\\" seq:EscapeSequenceCharacter { return seq, nil }
SingleQuotedStringCharacter <- !("'" / "\\") . { return string(c.text), nil }
/ "\\" seq:EscapeSequenceCharacter { return seq, nil }
EscapeSequenceCharacter <- char:EscapeCharacter EscapeSequenceCharacter <- char:EscapeCharacter
EscapeCharacter <- "'" EscapeCharacter <- "'" { return "'", nil }
/ '"' / '"' { return "\"", nil }
/ "\\" / "\\" { return "\\", nil }
/ "b" { return "\b", nil } / "b" { return "\b", nil }
/ "f" { return "\f", nil } / "f" { return "\f", nil }
/ "n" { return "\n", nil } / "n" { return "\n", nil }
+26
View File
@@ -149,6 +149,32 @@ func Test_Parse_Were(t *testing.T) {
) )
}) })
t.Run("Should correctly parse single-quoted string literals", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id FROM c WHERE c.id = '12345' AND c.string = 'it\'s'`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{{Path: []string{"c", "id"}}},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeAnd,
Expressions: []interface{}{
parsers.ComparisonExpression{
Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: testutils.SelectItem_Constant_String("12345"),
},
parsers.ComparisonExpression{
Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "string"}},
Right: testutils.SelectItem_Constant_String("it's"),
},
},
},
},
)
})
t.Run("Should correctly parse NOT conditions", func(t *testing.T) { t.Run("Should correctly parse NOT conditions", func(t *testing.T) {
testQueryParse( testQueryParse(
t, t,