Skip to content

Commit b0d0bd2

Browse files
authored
Cosmos DB: Support for parametrized queries (Azure#18577)
* Adding base query parameter * Exposing parameters * tests * docs * changelog * typo on changelog * Undo line change * Using literals * Using nil
1 parent 21f6363 commit b0d0bd2

File tree

10 files changed

+197
-6
lines changed

10 files changed

+197
-6
lines changed

sdk/data/azcosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features Added
66
* Added `NewClientFromConnectionString` function to create client from connection string
7+
* Added support for parametrized queries through `QueryOptions.QueryParameters`
78

89
### Breaking Changes
910

sdk/data/azcosmos/cosmos_client.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ func (c *Client) sendQueryRequest(
203203
path string,
204204
ctx context.Context,
205205
query string,
206+
parameters []QueryParameter,
206207
operationContext pipelineRequestOptions,
207208
requestOptions cosmosRequestOptions,
208209
requestEnricher func(*policy.Request)) (*http.Response, error) {
@@ -211,13 +212,11 @@ func (c *Client) sendQueryRequest(
211212
return nil, err
212213
}
213214

214-
type queryBody struct {
215-
Query string `json:"query"`
216-
}
217-
218215
err = azruntime.MarshalAsJSON(req, queryBody{
219-
Query: query,
216+
Query: query,
217+
Parameters: parameters,
220218
})
219+
221220
if err != nil {
222221
return nil, err
223222
}

sdk/data/azcosmos/cosmos_client_test.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ func TestSendQuery(t *testing.T) {
388388
resourceAddress: "",
389389
}
390390

391-
_, err := client.sendQueryRequest("/", context.Background(), "SELECT * FROM c", operationContext, &DeleteDatabaseOptions{}, nil)
391+
_, err := client.sendQueryRequest("/", context.Background(), "SELECT * FROM c", []QueryParameter{}, operationContext, &DeleteDatabaseOptions{}, nil)
392392
if err != nil {
393393
t.Fatal(err)
394394
}
@@ -410,6 +410,48 @@ func TestSendQuery(t *testing.T) {
410410
}
411411
}
412412

413+
func TestSendQueryWithParameters(t *testing.T) {
414+
srv, close := mock.NewTLSServer()
415+
defer close()
416+
srv.SetResponse(
417+
mock.WithStatusCode(200))
418+
verifier := pipelineVerifier{}
419+
pl := azruntime.NewPipeline("azcosmostest", "v1.0.0", azruntime.PipelineOptions{PerCall: []policy.Policy{&verifier}}, &policy.ClientOptions{Transport: srv})
420+
client := &Client{endpoint: srv.URL(), pipeline: pl}
421+
operationContext := pipelineRequestOptions{
422+
resourceType: resourceTypeDatabase,
423+
resourceAddress: "",
424+
}
425+
426+
parameters := []QueryParameter{
427+
{"@id", "1"},
428+
{"@status", "enabled"},
429+
}
430+
431+
_, err := client.sendQueryRequest("/", context.Background(), "SELECT * FROM c WHERE c.id = @id and c.status = @status", parameters, operationContext, &DeleteDatabaseOptions{}, nil)
432+
if err != nil {
433+
t.Fatal(err)
434+
}
435+
436+
if verifier.requests[0].method != http.MethodPost {
437+
t.Errorf("Expected %v, but got %v", http.MethodPost, verifier.requests[0].method)
438+
}
439+
440+
if verifier.requests[0].isQuery != true {
441+
t.Errorf("Expected %v, but got %v", true, verifier.requests[0].isQuery)
442+
}
443+
444+
if verifier.requests[0].contentType != cosmosHeaderValuesQuery {
445+
t.Errorf("Expected %v, but got %v", cosmosHeaderValuesQuery, verifier.requests[0].contentType)
446+
}
447+
448+
expectedSerializedQuery := "{\"query\":\"SELECT * FROM c WHERE c.id = @id and c.status = @status\",\"parameters\":[{\"name\":\"@id\",\"value\":\"1\"},{\"name\":\"@status\",\"value\":\"enabled\"}]}"
449+
450+
if verifier.requests[0].body != expectedSerializedQuery {
451+
t.Errorf("Expected %v, but got %v", expectedSerializedQuery, verifier.requests[0].body)
452+
}
453+
}
454+
413455
func TestSendBatch(t *testing.T) {
414456
srv, close := mock.NewTLSServer()
415457
defer close()

sdk/data/azcosmos/cosmos_container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ func (c *ContainerClient) NewQueryItemsPager(query string, partitionKey Partitio
444444
path,
445445
ctx,
446446
query,
447+
queryOptions.QueryParameters,
447448
operationContext,
448449
queryOptions,
449450
nil)

sdk/data/azcosmos/cosmos_offers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func (c cosmosOffers) ReadThroughputIfExists(
3838
path,
3939
ctx,
4040
fmt.Sprintf(`SELECT * FROM c WHERE c.offerResourceId = '%s'`, targetRID),
41+
nil,
4142
operationContext,
4243
requestOptions,
4344
nil)

sdk/data/azcosmos/cosmos_query.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package azcosmos
5+
6+
// QueryParameter represents a parameter for a parametrized query.
7+
type QueryParameter struct {
8+
// Name represents the name of the parameter in the parametrized query.
9+
Name string `json:"name"`
10+
// Value represents the value of the parameter in the parametrized query.
11+
Value any `json:"value"`
12+
}
13+
14+
type queryBody struct {
15+
Query string `json:"query"`
16+
Parameters []QueryParameter `json:"parameters,omitempty"`
17+
}

sdk/data/azcosmos/cosmos_query_request_options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ type QueryOptions struct {
3131
// ContinuationToken to be used to continue a previous query execution.
3232
// Obtained from QueryItemsResponse.ContinuationToken.
3333
ContinuationToken string
34+
// QueryParameters allows execution of parametrized queries.
35+
// See https://docs.microsoft.com/azure/cosmos-db/sql/sql-query-parameterized-queries
36+
QueryParameters []QueryParameter
3437
}
3538

3639
func (options *QueryOptions) toHeaders() *map[string]string {

sdk/data/azcosmos/doc.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ Querying items
144144
}
145145
}
146146
147+
Querying items with parametrized queries
148+
&opt := azcosmos.QueryOptions{
149+
azcosmos.QueryParameters: []QueryParameter{
150+
{"@value", "2"},
151+
},
152+
}
153+
pk := azcosmos.NewPartitionKeyString("myPartitionKeyValue")
154+
queryPager := container.NewQueryItemsPager("select * from docs c where c.value = @value", pk, opt)
155+
for queryPager.More() {
156+
queryResponse, err := queryPager.NextPage(context)
157+
if err != nil {
158+
handle(err)
159+
}
160+
161+
for _, item := range queryResponse.Items {
162+
var itemResponseBody map[string]interface{}
163+
json.Unmarshal(item, &itemResponseBody)
164+
}
165+
}
166+
147167
Using Transactional batch
148168
149169
pk := azcosmos.NewPartitionKeyString("myPartitionKeyValue")

sdk/data/azcosmos/emulator_cosmos_query_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,56 @@ func TestSinglePartitionQuery(t *testing.T) {
169169
}
170170
}
171171

172+
func TestSinglePartitionQueryWithParameters(t *testing.T) {
173+
emulatorTests := newEmulatorTests(t)
174+
client := emulatorTests.getClient(t)
175+
176+
database := emulatorTests.createDatabase(t, context.TODO(), client, "queryTests")
177+
defer emulatorTests.deleteDatabase(t, context.TODO(), database)
178+
properties := ContainerProperties{
179+
ID: "aContainer",
180+
PartitionKeyDefinition: PartitionKeyDefinition{
181+
Paths: []string{"/pk"},
182+
},
183+
}
184+
185+
_, err := database.CreateContainer(context.TODO(), properties, nil)
186+
if err != nil {
187+
t.Fatalf("Failed to create container: %v", err)
188+
}
189+
190+
container, _ := database.NewContainer("aContainer")
191+
documentsPerPk := 1
192+
createSampleItems(t, container, documentsPerPk)
193+
194+
receivedIds := []string{}
195+
opt := QueryOptions{
196+
QueryParameters: []QueryParameter{
197+
{"@prop", "2"},
198+
},
199+
}
200+
queryPager := container.NewQueryItemsPager("select * from c where c.someProp = @prop", NewPartitionKeyString("1"), &opt)
201+
for queryPager.More() {
202+
queryResponse, err := queryPager.NextPage(context.TODO())
203+
if err != nil {
204+
t.Fatalf("Failed to query items: %v", err)
205+
}
206+
207+
for _, item := range queryResponse.Items {
208+
var itemResponseBody map[string]interface{}
209+
err = json.Unmarshal(item, &itemResponseBody)
210+
if err != nil {
211+
t.Fatalf("Failed to unmarshal: %v", err)
212+
}
213+
receivedIds = append(receivedIds, itemResponseBody["id"].(string))
214+
}
215+
}
216+
217+
if len(receivedIds) != 1 {
218+
t.Fatalf("Expected 1 document, got %d", len(receivedIds))
219+
}
220+
}
221+
172222
func createSampleItems(t *testing.T, container *ContainerClient, documentsPerPk int) {
173223
for i := 0; i < documentsPerPk; i++ {
174224
item := map[string]string{

sdk/data/azcosmos/example_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,63 @@ func ExampleContainerClient_NewQueryItemsPager() {
615615
}
616616
}
617617

618+
// Azure Cosmos DB supports queries with parameters expressed by the familiar @ notation.
619+
// Parameterized SQL provides robust handling and escaping of user input, and prevents accidental exposure of data through SQL injection.
620+
func ExampleContainerClient_NewQueryItemsPager_parametrizedQueries() {
621+
endpoint, ok := os.LookupEnv("AZURE_COSMOS_ENDPOINT")
622+
if !ok {
623+
panic("AZURE_COSMOS_ENDPOINT could not be found")
624+
}
625+
626+
key, ok := os.LookupEnv("AZURE_COSMOS_KEY")
627+
if !ok {
628+
panic("AZURE_COSMOS_KEY could not be found")
629+
}
630+
631+
cred, err := azcosmos.NewKeyCredential(key)
632+
if err != nil {
633+
panic(err)
634+
}
635+
636+
client, err := azcosmos.NewClientWithKey(endpoint, cred, nil)
637+
if err != nil {
638+
panic(err)
639+
}
640+
641+
container, err := client.NewContainer("databaseName", "aContainer")
642+
if err != nil {
643+
panic(err)
644+
}
645+
646+
opt := &azcosmos.QueryOptions{
647+
QueryParameters: []azcosmos.QueryParameter{
648+
{"@value", "2"},
649+
},
650+
}
651+
652+
pk := azcosmos.NewPartitionKeyString("newPartitionKey")
653+
654+
queryPager := container.NewQueryItemsPager("select * from docs c where c.value = @value", pk, opt)
655+
for queryPager.More() {
656+
queryResponse, err := queryPager.NextPage(context.Background())
657+
if err != nil {
658+
var responseErr *azcore.ResponseError
659+
errors.As(err, &responseErr)
660+
panic(responseErr)
661+
}
662+
663+
for _, item := range queryResponse.Items {
664+
var itemResponseBody map[string]interface{}
665+
err = json.Unmarshal(item, &itemResponseBody)
666+
if err != nil {
667+
panic(err)
668+
}
669+
}
670+
671+
fmt.Printf("Query page received with %v items. ActivityId %s consuming %v RU", len(queryResponse.Items), queryResponse.ActivityID, queryResponse.RequestCharge)
672+
}
673+
}
674+
618675
func ExampleContainerClient_NewTransactionalBatch() {
619676
endpoint, ok := os.LookupEnv("AZURE_COSMOS_ENDPOINT")
620677
if !ok {

0 commit comments

Comments
 (0)