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 <pikami@users.noreply.github.com>

* 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 <pikami@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Pijus Kamandulis
2026-07-12 14:02:23 +03:00
committed by GitHub
parent 90de21c7b0
commit c78842726b
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) {
testCosmosQuery(t, collectionClient,
`select c.id
+1508 -1335
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* "\"" {
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]+ {
floatValue, _ := strconv.ParseFloat(string(c.text), 64)
@@ -869,11 +871,14 @@ Integer <- [0-9]+ {
StringCharacter <- !('"' / "\\") . { return string(c.text), nil }
/ "\\" seq:EscapeSequenceCharacter { return seq, nil }
SingleQuotedStringCharacter <- !("'" / "\\") . { return string(c.text), nil }
/ "\\" seq:EscapeSequenceCharacter { return seq, nil }
EscapeSequenceCharacter <- char:EscapeCharacter
EscapeCharacter <- "'"
/ '"'
/ "\\"
EscapeCharacter <- "'" { return "'", nil }
/ '"' { return "\"", nil }
/ "\\" { return "\\", nil }
/ "b" { return "\b", nil }
/ "f" { return "\f", 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) {
testQueryParse(
t,