From 6facdde8cbc68454e1cdef8666f7844c14f0f556 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sat, 17 Jun 2023 10:23:31 +1000 Subject: [PATCH] prepare to release v0.3.0 --- README.md | 2 +- RELEASE-NOTES.md | 2 +- data_test.go | 61 ++++++++++++++++++++++ gocosmos.go | 2 +- stmt_document_select_test.go | 98 ++++++++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9873ffc..6be28ec 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Summary of supported SQL statements: |Delete an existing database |`DROP DATABASE [IF EXISTS] `| |List all existing databases |`LIST DATABASES`| |Create a new collection |`CREATE COLLECTION [IF NOT EXISTS] [.] `| -|Change collection's throughput |`ALTER COLLECTION [.] WITH RU/MAXRU=`| +|Change collection's throughput |`ALTER COLLECTION [.] WITH RU|MAXRU=`| |Delete an existing collection |`DROP COLLECTION [IF EXISTS] [.]`| |List all existing collections in a database|`LIST COLLECTIONS [FROM ]`| |Insert a new document into collection |`INSERT INTO [.] ...`| diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f8f1c00..bff768a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,6 +1,6 @@ # gocosmos - Release notes -## 2003-06-0x - v0.3.0 +## 2023-06-16 - v0.3.0 - Change default API version to `2020-07-15`. - Add [Hierarchical Partition Keys](https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys) (sub-partitions) support. diff --git a/data_test.go b/data_test.go index 116b17d..6fa411b 100644 --- a/data_test.go +++ b/data_test.go @@ -14,11 +14,72 @@ import ( /*======================================================================*/ +const numApps = 4 const numLogicalPartitions = 16 const numCategories = 19 var dataList []DocInfo +func _initDataSubPartitions(t *testing.T, testName string, client *RestClient, db, container string, numItem int) { + totalRu := 0.0 + randList := make([]int, numItem) + for i := 0; i < numItem; i++ { + randList[i] = i*2 + 1 + } + rand.Shuffle(numItem, func(i, j int) { + randList[i], randList[j] = randList[j], randList[i] + }) + dataList = make([]DocInfo, numItem) + for i := 0; i < numItem; i++ { + category := randList[i] % numCategories + app := "app" + strconv.Itoa(i%numApps) + username := "user" + strconv.Itoa(i%numLogicalPartitions) + docInfo := DocInfo{ + "id": fmt.Sprintf("%05d", i), + "app": app, + "username": username, + "email": "user" + strconv.Itoa(i) + "@domain.com", + "grade": float64(randList[i]), + "category": float64(category), + "active": i%10 == 0, + "big": fmt.Sprintf("%05d", i) + "/" + strings.Repeat("this is a very long string/", 256), + } + dataList[i] = docInfo + if result := client.CreateDocument(DocumentSpec{DbName: db, CollName: container, PartitionKeyValues: []interface{}{app, username}, DocumentData: docInfo}); result.Error() != nil { + t.Fatalf("%s failed: %s", testName, result.Error()) + } else { + totalRu += result.RequestCharge + } + } + // fmt.Printf("\t%s - total RU charged: %0.3f\n", testName+"/Insert", totalRu) +} + +func _initDataSubPartitionsSmallRU(t *testing.T, testName string, client *RestClient, db, container string, numItem int) { + client.DeleteDatabase(db) + client.CreateDatabase(DatabaseSpec{Id: db, Ru: 400}) + client.CreateCollection(CollectionSpec{ + DbName: db, + CollName: container, + PartitionKeyInfo: map[string]interface{}{"paths": []string{"/app", "/username"}, "kind": "MultiHash", "version": 2}, + UniqueKeyPolicy: map[string]interface{}{"uniqueKeys": []map[string]interface{}{{"paths": []string{"/email"}}}}, + Ru: 400, + }) + _initDataSubPartitions(t, testName, client, db, container, numItem) +} + +func _initDataSubPartitionsLargeRU(t *testing.T, testName string, client *RestClient, db, container string, numItem int) { + client.DeleteDatabase(db) + client.CreateDatabase(DatabaseSpec{Id: db, Ru: 20000}) + client.CreateCollection(CollectionSpec{ + DbName: db, + CollName: container, + PartitionKeyInfo: map[string]interface{}{"paths": []string{"/app", "/username"}, "kind": "MultiHash", "version": 2}, + UniqueKeyPolicy: map[string]interface{}{"uniqueKeys": []map[string]interface{}{{"paths": []string{"/email"}}}}, + Ru: 20000, + }) + _initDataSubPartitions(t, testName, client, db, container, numItem) +} + func _initData(t *testing.T, testName string, client *RestClient, db, container string, numItem int) { totalRu := 0.0 randList := make([]int, numItem) diff --git a/gocosmos.go b/gocosmos.go index ee41912..0fbc91f 100644 --- a/gocosmos.go +++ b/gocosmos.go @@ -7,7 +7,7 @@ import ( const ( // Version of package gocosmos. - Version = "0.2.1" + Version = "0.3.0" ) func goTypeToCosmosDbType(typ reflect.Type) string { diff --git a/stmt_document_select_test.go b/stmt_document_select_test.go index dda1399..e41f573 100644 --- a/stmt_document_select_test.go +++ b/stmt_document_select_test.go @@ -25,6 +25,104 @@ func TestStmtSelect_Exec(t *testing.T) { /*----------------------------------------------------------------------*/ +func _testSelectPkValueSubPartitions(t *testing.T, testName string, db *sql.DB, collname string) { + low, high := 123, 987 + lowStr, highStr := fmt.Sprintf("%05d", low), fmt.Sprintf("%05d", high) + countPerPartition := _countPerPartition(low, high, dataList) + distinctPerPartition := _distinctPerPartition(low, high, dataList, "category") + var testCases = []queryTestCase{ + {name: "NoLimit_Bare", query: "SELECT * FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 WITH collection=%s WITH cross_partition=true"}, + {name: "OffsetLimit_Bare", query: "SELECT * FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 OFFSET 3 LIMIT 5 WITH collection=%s WITH cross_partition=true", expectedNumItems: 5}, + {name: "NoLimit_OrderAsc", query: "SELECT * FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 ORDER BY c.grade WITH collection=%s WITH cross_partition=true", orderType: reddo.TypeInt, orderField: "grade", orderDirection: "asc"}, + {name: "OffsetLimit_OrderDesc", query: "SELECT * FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 ORDER BY c.category DESC OFFSET 3 LIMIT 5 WITH collection=%s WITH cross_partition=true", expectedNumItems: 5, orderType: reddo.TypeInt, orderField: "category", orderDirection: "desc"}, + + {name: "NoLimit_DistinctValue", query: "SELECT DISTINCT VALUE c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 WITH collection=%s WITH cross_partition=true", distinctQuery: 1}, + {name: "NoLimit_DistinctDoc", query: "SELECT DISTINCT c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 WITH collection=%s WITH cross_partition=true", distinctQuery: -1}, + {name: "OffsetLimit_DistinctValue", query: "SELECT DISTINCT VALUE c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", distinctQuery: 1, expectedNumItems: 3}, + {name: "OffsetLimit_DistinctDoc", query: "SELECT DISTINCT c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", distinctQuery: -1, expectedNumItems: 3}, + + {name: "NoLimit_DistinctValue_OrderAsc", query: "SELECT DISTINCT VALUE c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 ORDER BY c.category WITH collection=%s WITH cross_partition=true", distinctQuery: 1, orderType: reddo.TypeInt, orderField: "$1", orderDirection: "asc"}, + {name: "NoLimit_DistinctDoc_OrderDesc", query: "SELECT DISTINCT c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 ORDER BY c.category DESC WITH collection=%s WITH cross_partition=true", distinctQuery: -1, orderType: reddo.TypeInt, orderField: "category", orderDirection: "desc"}, + {name: "OffsetLimit_DistinctValue_OrderAsc", query: "SELECT DISTINCT VALUE c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 ORDER BY c.category OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", distinctQuery: 1, orderType: reddo.TypeInt, orderField: "$1", orderDirection: "asc", expectedNumItems: 3}, + {name: "OffsetLimit_DistinctDoc_OrderDesc", query: "SELECT DISTINCT c.category FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 ORDER BY c.category DESC OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", distinctQuery: -1, orderType: reddo.TypeInt, orderField: "category", orderDirection: "desc", expectedNumItems: 3}, + + /* GROUP BY with ORDER BY is not supported! */ + {name: "NoLimit_GroupByCount", query: "SELECT c.category AS 'Category', count(1) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category WITH collection=%s WITH cross_partition=true", groupByAggr: "count"}, + {name: "OffsetLimit_GroupByCount", query: "SELECT c.category AS 'Category', count(1) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", expectedNumItems: 3, groupByAggr: "count"}, + {name: "NoLimit_GroupBySum", query: "SELECT c.category AS 'Category', sum(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category WITH collection=%s WITH cross_partition=true", groupByAggr: "sum"}, + {name: "OffsetLimit_GroupBySum", query: "SELECT c.category AS 'Category', sum(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", expectedNumItems: 3, groupByAggr: "sum"}, + {name: "NoLimit_GroupByMin", query: "SELECT c.category AS 'Category', min(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category WITH collection=%s WITH cross_partition=true", groupByAggr: "min"}, + {name: "OffsetLimit_GroupByMin", query: "SELECT c.category AS 'Category', min(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", expectedNumItems: 3, groupByAggr: "min"}, + {name: "NoLimit_GroupByMax", query: "SELECT c.category AS 'Category', max(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category WITH collection=%s WITH cross_partition=true", groupByAggr: "max"}, + {name: "OffsetLimit_GroupByMax", query: "SELECT c.category AS 'Category', max(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", expectedNumItems: 3, groupByAggr: "max"}, + {name: "NoLimit_GroupByAvg", query: "SELECT c.category AS 'Category', avg(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category WITH collection=%s WITH cross_partition=true", groupByAggr: "average"}, + {name: "OffsetLimit_GroupByAvg", query: "SELECT c.category AS 'Category', avg(c.grade) AS 'Value' FROM c WHERE $1<=c.id AND c.id<@2 AND c.username=:3 GROUP BY c.category OFFSET 1 LIMIT 3 WITH collection=%s WITH cross_partition=true", expectedNumItems: 3, groupByAggr: "average"}, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + savedExpectedNumItems := testCase.expectedNumItems + for i := 0; i < numLogicalPartitions; i++ { + testCase.expectedNumItems = savedExpectedNumItems + expectedNumItems := testCase.expectedNumItems + username := "user" + strconv.Itoa(i) + params := []interface{}{lowStr, highStr, username} + if expectedNumItems <= 0 && testCase.maxItemCount <= 0 { + expectedNumItems = countPerPartition[username] + if testCase.distinctQuery != 0 { + expectedNumItems = distinctPerPartition[username] + } + testCase.expectedNumItems = expectedNumItems + } + sql := fmt.Sprintf(testCase.query, collname) + dbRows, err := db.Query(sql, params...) + if err != nil { + t.Fatalf("%s failed: %s", testName+"/"+testCase.name, err) + } + rows, err := _fetchAllRows(dbRows) + if err != nil { + t.Fatalf("%s failed: %s", testName+"/"+testCase.name, err) + } + _verifyResult(func(msg string) { t.Fatal(msg) }, testName+"/"+testCase.name+"/pk="+username, testCase, expectedNumItems, rows) + _verifyDistinct(func(msg string) { t.Fatal(msg) }, testName+"/"+testCase.name+"/pk="+username, testCase, rows) + _verifyOrderBy(func(msg string) { t.Fatal(msg) }, testName+"/"+testCase.name+"/pk="+username, testCase, rows) + _verifyGroupBy(func(msg string) { t.Fatal(msg) }, testName+"/"+testCase.name+"/pk="+username, testCase, username, lowStr, highStr, rows) + } + }) + } +} + +func TestStmtSelect_Query_PkValue_SubPartitions_SmallRU(t *testing.T) { + testName := "TestStmtSelect_Query_PkValue_SmallRU" + dbname := testDb + collname := testTable + client := _newRestClient(t, testName) + _initDataSubPartitionsSmallRU(t, testName, client, dbname, collname, 1000) + if result := client.GetPkranges(dbname, collname); result.Error() != nil { + t.Fatalf("%s failed: %s", testName+"/GetPkranges", result.Error()) + } else if result.Count != 1 { + t.Fatalf("%s failed: expected to be %#v but received %#v", testName+"/GetPkranges", 1, result.Count) + } + db := _openDefaultDb(t, testName, dbname) + _testSelectPkValueSubPartitions(t, testName, db, collname) +} + +func TestStmtSelect_Query_PkValue_SubPartitions_LargeRU(t *testing.T) { + testName := "TestStmtSelect_Query_PkValue_LargeRU" + dbname := testDb + collname := testTable + client := _newRestClient(t, testName) + _initDataSubPartitionsLargeRU(t, testName, client, dbname, collname, 1000) + if result := client.GetPkranges(dbname, collname); result.Error() != nil { + t.Fatalf("%s failed: %s", testName+"/GetPkranges", result.Error()) + } else if result.Count < 2 { + t.Fatalf("%s failed: expected to be larger than %#v but received %#v", testName+"/GetPkranges", 1, result.Count) + } + db := _openDefaultDb(t, testName, dbname) + _testSelectPkValueSubPartitions(t, testName, db, collname) +} + +/*----------------------------------------------------------------------*/ + func _testSelectPkValue(t *testing.T, testName string, db *sql.DB, collname string) { low, high := 123, 987 lowStr, highStr := fmt.Sprintf("%05d", low), fmt.Sprintf("%05d", high)