diff --git a/dev/conformance/conformance-tests/create-all-transforms.json b/dev/conformance/conformance-tests/create-all-transforms.json index 638959998..82831624b 100644 --- a/dev/conformance/conformance-tests/create-all-transforms.json +++ b/dev/conformance/conformance-tests/create-all-transforms.json @@ -20,45 +20,50 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-arrayremove-multi.json b/dev/conformance/conformance-tests/create-arrayremove-multi.json index 331a53bf9..548a98380 100644 --- a/dev/conformance/conformance-tests/create-arrayremove-multi.json +++ b/dev/conformance/conformance-tests/create-arrayremove-multi.json @@ -20,41 +20,46 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-arrayremove-nested.json b/dev/conformance/conformance-tests/create-arrayremove-nested.json index 00c73d05c..fa01bd7e0 100644 --- a/dev/conformance/conformance-tests/create-arrayremove-nested.json +++ b/dev/conformance/conformance-tests/create-arrayremove-nested.json @@ -20,25 +20,30 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-arrayremove.json b/dev/conformance/conformance-tests/create-arrayremove.json index 646e259f6..a69be14b7 100644 --- a/dev/conformance/conformance-tests/create-arrayremove.json +++ b/dev/conformance/conformance-tests/create-arrayremove.json @@ -20,25 +20,30 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-arrayunion-multi.json b/dev/conformance/conformance-tests/create-arrayunion-multi.json index 5ba324f42..7ca9852f4 100644 --- a/dev/conformance/conformance-tests/create-arrayunion-multi.json +++ b/dev/conformance/conformance-tests/create-arrayunion-multi.json @@ -20,41 +20,46 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "appendMissingElements": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "appendMissingElements": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-arrayunion-nested.json b/dev/conformance/conformance-tests/create-arrayunion-nested.json index 2a2150900..a2f20299d 100644 --- a/dev/conformance/conformance-tests/create-arrayunion-nested.json +++ b/dev/conformance/conformance-tests/create-arrayunion-nested.json @@ -20,25 +20,30 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-arrayunion.json b/dev/conformance/conformance-tests/create-arrayunion.json index 99a75fede..26d079946 100644 --- a/dev/conformance/conformance-tests/create-arrayunion.json +++ b/dev/conformance/conformance-tests/create-arrayunion.json @@ -20,25 +20,30 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-st-alone.json b/dev/conformance/conformance-tests/create-st-alone.json index 177293906..20c5e8ec3 100644 --- a/dev/conformance/conformance-tests/create-st-alone.json +++ b/dev/conformance/conformance-tests/create-st-alone.json @@ -10,19 +10,18 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "setToServerValue": "REQUEST_TIME" + } + ] + }, "currentDocument": { "exists": false - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateTransforms": [ - { - "fieldPath": "a", - "setToServerValue": "REQUEST_TIME" - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-st-multi.json b/dev/conformance/conformance-tests/create-st-multi.json index 41f3cd811..89430e2b6 100644 --- a/dev/conformance/conformance-tests/create-st-multi.json +++ b/dev/conformance/conformance-tests/create-st-multi.json @@ -20,17 +20,22 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c.d", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c.d", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-st-nested.json b/dev/conformance/conformance-tests/create-st-nested.json index 7316d916f..f2a3a8d1f 100644 --- a/dev/conformance/conformance-tests/create-st-nested.json +++ b/dev/conformance/conformance-tests/create-st-nested.json @@ -20,13 +20,18 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-st-with-empty-map.json b/dev/conformance/conformance-tests/create-st-with-empty-map.json index b638a0c9d..730afd154 100644 --- a/dev/conformance/conformance-tests/create-st-with-empty-map.json +++ b/dev/conformance/conformance-tests/create-st-with-empty-map.json @@ -28,13 +28,18 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "a.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/create-st.json b/dev/conformance/conformance-tests/create-st.json index c4ad4be46..705f76ed1 100644 --- a/dev/conformance/conformance-tests/create-st.json +++ b/dev/conformance/conformance-tests/create-st.json @@ -20,13 +20,18 @@ }, "currentDocument": { "exists": false - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-all-transforms.json b/dev/conformance/conformance-tests/set-all-transforms.json index a26b51b00..5c8b1373d 100644 --- a/dev/conformance/conformance-tests/set-all-transforms.json +++ b/dev/conformance/conformance-tests/set-all-transforms.json @@ -17,45 +17,50 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-arrayremove-multi.json b/dev/conformance/conformance-tests/set-arrayremove-multi.json index dc2ace22f..3ea9b0dbd 100644 --- a/dev/conformance/conformance-tests/set-arrayremove-multi.json +++ b/dev/conformance/conformance-tests/set-arrayremove-multi.json @@ -17,41 +17,46 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-arrayremove-nested.json b/dev/conformance/conformance-tests/set-arrayremove-nested.json index 1e25b8f26..4db133f2c 100644 --- a/dev/conformance/conformance-tests/set-arrayremove-nested.json +++ b/dev/conformance/conformance-tests/set-arrayremove-nested.json @@ -17,25 +17,30 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-arrayremove.json b/dev/conformance/conformance-tests/set-arrayremove.json index e0506b22b..18969ef80 100644 --- a/dev/conformance/conformance-tests/set-arrayremove.json +++ b/dev/conformance/conformance-tests/set-arrayremove.json @@ -17,25 +17,30 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-arrayunion-multi.json b/dev/conformance/conformance-tests/set-arrayunion-multi.json index 502d7dc7d..3d076397c 100644 --- a/dev/conformance/conformance-tests/set-arrayunion-multi.json +++ b/dev/conformance/conformance-tests/set-arrayunion-multi.json @@ -17,41 +17,46 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "appendMissingElements": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "appendMissingElements": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-arrayunion-nested.json b/dev/conformance/conformance-tests/set-arrayunion-nested.json index 7084e6bcd..e265f6c61 100644 --- a/dev/conformance/conformance-tests/set-arrayunion-nested.json +++ b/dev/conformance/conformance-tests/set-arrayunion-nested.json @@ -17,25 +17,30 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-arrayunion.json b/dev/conformance/conformance-tests/set-arrayunion.json index af12b33dd..856e07517 100644 --- a/dev/conformance/conformance-tests/set-arrayunion.json +++ b/dev/conformance/conformance-tests/set-arrayunion.json @@ -17,25 +17,30 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-alone-mergeall.json b/dev/conformance/conformance-tests/set-st-alone-mergeall.json index f6b60af81..d95bf0973 100644 --- a/dev/conformance/conformance-tests/set-st-alone-mergeall.json +++ b/dev/conformance/conformance-tests/set-st-alone-mergeall.json @@ -13,19 +13,15 @@ "database": "projects/projectID/databases/(default)", "writes": [ { - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "a", - "setToServerValue": "REQUEST_TIME" - } - ] + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-alone.json b/dev/conformance/conformance-tests/set-st-alone.json index 1d28fd6f1..3fe931394 100644 --- a/dev/conformance/conformance-tests/set-st-alone.json +++ b/dev/conformance/conformance-tests/set-st-alone.json @@ -13,13 +13,18 @@ "update": { "name": "projects/projectID/databases/(default)/documents/C/d", "fields": {} - }, - "updateTransforms": [ - { - "fieldPath": "a", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-merge-both.json b/dev/conformance/conformance-tests/set-st-merge-both.json index 359c899a1..a39ada55f 100644 --- a/dev/conformance/conformance-tests/set-st-merge-both.json +++ b/dev/conformance/conformance-tests/set-st-merge-both.json @@ -36,13 +36,18 @@ "fieldPaths": [ "a" ] - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-merge-nonleaf-alone.json b/dev/conformance/conformance-tests/set-st-merge-nonleaf-alone.json index 5af99ab0a..4193b00ea 100644 --- a/dev/conformance/conformance-tests/set-st-merge-nonleaf-alone.json +++ b/dev/conformance/conformance-tests/set-st-merge-nonleaf-alone.json @@ -26,13 +26,18 @@ "fieldPaths": [ "h" ] - }, - "updateTransforms": [ - { - "fieldPath": "h.g", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "h.g", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-merge-nonleaf.json b/dev/conformance/conformance-tests/set-st-merge-nonleaf.json index e66ca87bf..5e91d663b 100644 --- a/dev/conformance/conformance-tests/set-st-merge-nonleaf.json +++ b/dev/conformance/conformance-tests/set-st-merge-nonleaf.json @@ -37,13 +37,18 @@ "fieldPaths": [ "h" ] - }, - "updateTransforms": [ - { - "fieldPath": "h.g", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "h.g", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-merge-nowrite.json b/dev/conformance/conformance-tests/set-st-merge-nowrite.json index 44091b127..08fa8b52f 100644 --- a/dev/conformance/conformance-tests/set-st-merge-nowrite.json +++ b/dev/conformance/conformance-tests/set-st-merge-nowrite.json @@ -19,19 +19,15 @@ "database": "projects/projectID/databases/(default)", "writes": [ { - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-mergeall.json b/dev/conformance/conformance-tests/set-st-mergeall.json index f913d69e6..26883c038 100644 --- a/dev/conformance/conformance-tests/set-st-mergeall.json +++ b/dev/conformance/conformance-tests/set-st-mergeall.json @@ -25,13 +25,18 @@ "fieldPaths": [ "a" ] - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-multi.json b/dev/conformance/conformance-tests/set-st-multi.json index 03200729c..23c06f497 100644 --- a/dev/conformance/conformance-tests/set-st-multi.json +++ b/dev/conformance/conformance-tests/set-st-multi.json @@ -17,17 +17,22 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c.d", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c.d", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-nested.json b/dev/conformance/conformance-tests/set-st-nested.json index 58406e80b..5c94c33f9 100644 --- a/dev/conformance/conformance-tests/set-st-nested.json +++ b/dev/conformance/conformance-tests/set-st-nested.json @@ -17,13 +17,18 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st-with-empty-map.json b/dev/conformance/conformance-tests/set-st-with-empty-map.json index a40786653..063c94a0e 100644 --- a/dev/conformance/conformance-tests/set-st-with-empty-map.json +++ b/dev/conformance/conformance-tests/set-st-with-empty-map.json @@ -25,13 +25,18 @@ } } } - }, - "updateTransforms": [ - { - "fieldPath": "a.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/set-st.json b/dev/conformance/conformance-tests/set-st.json index 3e55ae111..42f2b14f1 100644 --- a/dev/conformance/conformance-tests/set-st.json +++ b/dev/conformance/conformance-tests/set-st.json @@ -17,13 +17,18 @@ "integerValue": "1" } } - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-all-transforms.json b/dev/conformance/conformance-tests/update-all-transforms.json index 72b16d3a1..6f6a725df 100644 --- a/dev/conformance/conformance-tests/update-all-transforms.json +++ b/dev/conformance/conformance-tests/update-all-transforms.json @@ -25,45 +25,50 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayremove-alone.json b/dev/conformance/conformance-tests/update-arrayremove-alone.json index 93b8ff052..86fc8802e 100644 --- a/dev/conformance/conformance-tests/update-arrayremove-alone.json +++ b/dev/conformance/conformance-tests/update-arrayremove-alone.json @@ -10,35 +10,31 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "a", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - } - ] - } + } + } ] } } diff --git a/dev/conformance/conformance-tests/update-arrayremove-multi.json b/dev/conformance/conformance-tests/update-arrayremove-multi.json index 18ed0fdde..df880f679 100644 --- a/dev/conformance/conformance-tests/update-arrayremove-multi.json +++ b/dev/conformance/conformance-tests/update-arrayremove-multi.json @@ -26,41 +26,46 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayremove-nested.json b/dev/conformance/conformance-tests/update-arrayremove-nested.json index 7159797c7..28d59aff6 100644 --- a/dev/conformance/conformance-tests/update-arrayremove-nested.json +++ b/dev/conformance/conformance-tests/update-arrayremove-nested.json @@ -26,25 +26,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayremove.json b/dev/conformance/conformance-tests/update-arrayremove.json index 2311f916d..d925704db 100644 --- a/dev/conformance/conformance-tests/update-arrayremove.json +++ b/dev/conformance/conformance-tests/update-arrayremove.json @@ -25,25 +25,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayunion-alone.json b/dev/conformance/conformance-tests/update-arrayunion-alone.json index 5cb08579c..757ea48c3 100644 --- a/dev/conformance/conformance-tests/update-arrayunion-alone.json +++ b/dev/conformance/conformance-tests/update-arrayunion-alone.json @@ -10,34 +10,30 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - }, - "fieldPath": "a" - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayunion-multi.json b/dev/conformance/conformance-tests/update-arrayunion-multi.json index 674ce2b4c..3aafcd0f3 100644 --- a/dev/conformance/conformance-tests/update-arrayunion-multi.json +++ b/dev/conformance/conformance-tests/update-arrayunion-multi.json @@ -26,41 +26,46 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "appendMissingElements": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "appendMissingElements": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayunion-nested.json b/dev/conformance/conformance-tests/update-arrayunion-nested.json index 841ceed0a..f2bf3770d 100644 --- a/dev/conformance/conformance-tests/update-arrayunion-nested.json +++ b/dev/conformance/conformance-tests/update-arrayunion-nested.json @@ -26,25 +26,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-arrayunion.json b/dev/conformance/conformance-tests/update-arrayunion.json index 0aca2356c..60192c9f8 100644 --- a/dev/conformance/conformance-tests/update-arrayunion.json +++ b/dev/conformance/conformance-tests/update-arrayunion.json @@ -25,25 +25,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-nested-transform-and-nested-value.json b/dev/conformance/conformance-tests/update-nested-transform-and-nested-value.json index 2ccba0985..ff7bfc6ee 100644 --- a/dev/conformance/conformance-tests/update-nested-transform-and-nested-value.json +++ b/dev/conformance/conformance-tests/update-nested-transform-and-nested-value.json @@ -31,13 +31,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "a.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-all-transforms.json b/dev/conformance/conformance-tests/update-paths-all-transforms.json index 40adbcaf5..01a4c1143 100644 --- a/dev/conformance/conformance-tests/update-paths-all-transforms.json +++ b/dev/conformance/conformance-tests/update-paths-all-transforms.json @@ -52,45 +52,50 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayremove-alone.json b/dev/conformance/conformance-tests/update-paths-arrayremove-alone.json index 4097f5888..9bc8a1440 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayremove-alone.json +++ b/dev/conformance/conformance-tests/update-paths-arrayremove-alone.json @@ -19,34 +19,30 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "a", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayremove-multi.json b/dev/conformance/conformance-tests/update-paths-arrayremove-multi.json index 5e76d07ba..9a8547120 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayremove-multi.json +++ b/dev/conformance/conformance-tests/update-paths-arrayremove-multi.json @@ -47,41 +47,46 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "removeAllFromArray": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "removeAllFromArray": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayremove-nested.json b/dev/conformance/conformance-tests/update-paths-arrayremove-nested.json index 9ee1b2a6f..e7f952ec3 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayremove-nested.json +++ b/dev/conformance/conformance-tests/update-paths-arrayremove-nested.json @@ -41,25 +41,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayremove.json b/dev/conformance/conformance-tests/update-paths-arrayremove.json index a7be888da..673a2ca2c 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayremove.json +++ b/dev/conformance/conformance-tests/update-paths-arrayremove.json @@ -40,25 +40,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "removeAllFromArray": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "removeAllFromArray": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayunion-alone.json b/dev/conformance/conformance-tests/update-paths-arrayunion-alone.json index 2375d0ced..81e1e9771 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayunion-alone.json +++ b/dev/conformance/conformance-tests/update-paths-arrayunion-alone.json @@ -19,34 +19,30 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - }, - "fieldPath": "a" - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayunion-multi.json b/dev/conformance/conformance-tests/update-paths-arrayunion-multi.json index afb643741..ef421bdad 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayunion-multi.json +++ b/dev/conformance/conformance-tests/update-paths-arrayunion-multi.json @@ -47,41 +47,46 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] - } - }, - { - "fieldPath": "c.d", - "appendMissingElements": { - "values": [ - { - "integerValue": "4" - }, - { - "integerValue": "5" - }, - { - "integerValue": "6" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } + }, + { + "fieldPath": "c.d", + "appendMissingElements": { + "values": [ + { + "integerValue": "4" + }, + { + "integerValue": "5" + }, + { + "integerValue": "6" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayunion-nested.json b/dev/conformance/conformance-tests/update-paths-arrayunion-nested.json index d908d0205..2d73527a4 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayunion-nested.json +++ b/dev/conformance/conformance-tests/update-paths-arrayunion-nested.json @@ -41,25 +41,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-arrayunion.json b/dev/conformance/conformance-tests/update-paths-arrayunion.json index ed2966aed..1401993d0 100644 --- a/dev/conformance/conformance-tests/update-paths-arrayunion.json +++ b/dev/conformance/conformance-tests/update-paths-arrayunion.json @@ -40,25 +40,30 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "appendMissingElements": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - }, - { - "integerValue": "3" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "appendMissingElements": { + "values": [ + { + "integerValue": "1" + }, + { + "integerValue": "2" + }, + { + "integerValue": "3" + } + ] + } } - } - ] + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-nested-transform-and-nested-value.json b/dev/conformance/conformance-tests/update-paths-nested-transform-and-nested-value.json index c4dead09e..927d783ae 100644 --- a/dev/conformance/conformance-tests/update-paths-nested-transform-and-nested-value.json +++ b/dev/conformance/conformance-tests/update-paths-nested-transform-and-nested-value.json @@ -48,13 +48,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "a.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-st-alone.json b/dev/conformance/conformance-tests/update-paths-st-alone.json index 668c1c932..085d04987 100644 --- a/dev/conformance/conformance-tests/update-paths-st-alone.json +++ b/dev/conformance/conformance-tests/update-paths-st-alone.json @@ -19,22 +19,18 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "setToServerValue": "REQUEST_TIME" + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "a", - "setToServerValue": "REQUEST_TIME" - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-st-multi.json b/dev/conformance/conformance-tests/update-paths-st-multi.json index 8767cf349..2d813801a 100644 --- a/dev/conformance/conformance-tests/update-paths-st-multi.json +++ b/dev/conformance/conformance-tests/update-paths-st-multi.json @@ -47,17 +47,22 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c.d", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c.d", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-st-nested.json b/dev/conformance/conformance-tests/update-paths-st-nested.json index 94ecaccaa..8bd35c911 100644 --- a/dev/conformance/conformance-tests/update-paths-st-nested.json +++ b/dev/conformance/conformance-tests/update-paths-st-nested.json @@ -41,13 +41,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-st-with-empty-map.json b/dev/conformance/conformance-tests/update-paths-st-with-empty-map.json index a86ae46cd..a4b8ed131 100644 --- a/dev/conformance/conformance-tests/update-paths-st-with-empty-map.json +++ b/dev/conformance/conformance-tests/update-paths-st-with-empty-map.json @@ -41,14 +41,25 @@ ] }, "updateTransforms": [ - { - "fieldPath": "a.c", - "setToServerValue": "REQUEST_TIME" - } + { + "fieldPath": "a.c", + "setToServerValue": 1 + } ], "currentDocument": { "exists": true } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-paths-st.json b/dev/conformance/conformance-tests/update-paths-st.json index 1710508b2..011405b9b 100644 --- a/dev/conformance/conformance-tests/update-paths-st.json +++ b/dev/conformance/conformance-tests/update-paths-st.json @@ -40,13 +40,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-st-alone.json b/dev/conformance/conformance-tests/update-st-alone.json index 49fab1769..1a333f30c 100644 --- a/dev/conformance/conformance-tests/update-st-alone.json +++ b/dev/conformance/conformance-tests/update-st-alone.json @@ -10,22 +10,18 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a", + "setToServerValue": "REQUEST_TIME" + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "a", - "setToServerValue": "REQUEST_TIME" - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-st-dot.json b/dev/conformance/conformance-tests/update-st-dot.json index 8b9a76902..83422ca52 100644 --- a/dev/conformance/conformance-tests/update-st-dot.json +++ b/dev/conformance/conformance-tests/update-st-dot.json @@ -10,22 +10,18 @@ "database": "projects/projectID/databases/(default)", "writes": [ { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.b.c", + "setToServerValue": "REQUEST_TIME" + } + ] + }, "currentDocument": { "exists": true - }, - "update": { - "fields": {}, - "name": "projects/projectID/databases/(default)/documents/C/d" - }, - "updateMask": { - "fieldPaths": [] - }, - "updateTransforms": [ - { - "fieldPath": "a.b.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-st-multi.json b/dev/conformance/conformance-tests/update-st-multi.json index f474112b6..8105ec27f 100644 --- a/dev/conformance/conformance-tests/update-st-multi.json +++ b/dev/conformance/conformance-tests/update-st-multi.json @@ -26,17 +26,22 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - }, - { - "fieldPath": "c.d", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + }, + { + "fieldPath": "c.d", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-st-nested.json b/dev/conformance/conformance-tests/update-st-nested.json index fa9f46b49..5a8e73237 100644 --- a/dev/conformance/conformance-tests/update-st-nested.json +++ b/dev/conformance/conformance-tests/update-st-nested.json @@ -26,13 +26,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-st-with-empty-map.json b/dev/conformance/conformance-tests/update-st-with-empty-map.json index 4a2c27dfb..abeceb03e 100644 --- a/dev/conformance/conformance-tests/update-st-with-empty-map.json +++ b/dev/conformance/conformance-tests/update-st-with-empty-map.json @@ -33,13 +33,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "a.c", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "a.c", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/conformance-tests/update-st.json b/dev/conformance/conformance-tests/update-st.json index 71d17f3c7..6249d8bda 100644 --- a/dev/conformance/conformance-tests/update-st.json +++ b/dev/conformance/conformance-tests/update-st.json @@ -25,13 +25,18 @@ }, "currentDocument": { "exists": true - }, - "updateTransforms": [ - { - "fieldPath": "b", - "setToServerValue": "REQUEST_TIME" - } - ] + } + }, + { + "transform": { + "document": "projects/projectID/databases/(default)/documents/C/d", + "fieldTransforms": [ + { + "fieldPath": "b", + "setToServerValue": "REQUEST_TIME" + } + ] + } } ] } diff --git a/dev/conformance/runner.ts b/dev/conformance/runner.ts index f5546acc1..0e925aec6 100644 --- a/dev/conformance/runner.ts +++ b/dev/conformance/runner.ts @@ -50,7 +50,10 @@ import api = proto.google.firestore.v1; type ConformanceProto = any; // tslint:disable-line:no-any /** List of test cases that are ignored. */ -const ignoredRe: RegExp[] = []; +const ignoredRe: RegExp[] = [ + // TODO(chenbrian): Enable this test once update_transforms support is added. + new RegExp('update-paths: ServerTimestamp beside an empty map'), +]; /** If non-empty, list the test cases to run exclusively. */ const exclusiveRe: RegExp[] = []; diff --git a/dev/protos/firestore_v1_proto_api.d.ts b/dev/protos/firestore_v1_proto_api.d.ts index 25d163597..0531279d2 100644 --- a/dev/protos/firestore_v1_proto_api.d.ts +++ b/dev/protos/firestore_v1_proto_api.d.ts @@ -1966,20 +1966,6 @@ export namespace google { public listCollectionIds(request: google.firestore.v1.IListCollectionIdsRequest): Promise; /** - * Calls BatchWrite. - * @param request BatchWriteRequest message or plain object - * @param callback Node-style callback called with the error, if any, and BatchWriteResponse - */ - public batchWrite(request: google.firestore.v1.IBatchWriteRequest, callback: google.firestore.v1.Firestore.BatchWriteCallback): void; - - /** - * Calls BatchWrite. - * @param request BatchWriteRequest message or plain object - * @returns Promise - */ - public batchWrite(request: google.firestore.v1.IBatchWriteRequest): Promise; - - /* * Calls CreateDocument. * @param request CreateDocumentRequest message or plain object * @param callback Node-style callback called with the error, if any, and Document @@ -2081,13 +2067,6 @@ export namespace google { type ListCollectionIdsCallback = (error: (Error|null), response?: google.firestore.v1.ListCollectionIdsResponse) => void; /** - * Callback as used by {@link google.firestore.v1.Firestore#batchWrite}. - * @param error Error, if any - * @param [response] BatchWriteResponse - */ - type BatchWriteCallback = (error: (Error|null), response?: google.firestore.v1.BatchWriteResponse) => void; - - /* * Callback as used by {@link google.firestore.v1.Firestore#createDocument}. * @param error Error, if any * @param [response] Document @@ -3040,58 +3019,6 @@ export namespace google { public nextPageToken: string; } - /** Properties of a BatchWriteRequest. */ - interface IBatchWriteRequest { - - /** BatchWriteRequest database */ - database?: (string|null); - - /** BatchWriteRequest writes */ - writes?: (google.firestore.v1.IWrite[]|null); - } - - /** Represents a BatchWriteRequest. */ - class BatchWriteRequest implements IBatchWriteRequest { - - /** - * Constructs a new BatchWriteRequest. - * @param [properties] Properties to set - */ - constructor(properties?: google.firestore.v1.IBatchWriteRequest); - - /** BatchWriteRequest database. */ - public database: string; - - /** BatchWriteRequest writes. */ - public writes: google.firestore.v1.IWrite[]; - } - - /** Properties of a BatchWriteResponse. */ - interface IBatchWriteResponse { - - /** BatchWriteResponse writeResults */ - writeResults?: (google.firestore.v1.IWriteResult[]|null); - - /** BatchWriteResponse status */ - status?: (google.rpc.IStatus[]|null); - } - - /** Represents a BatchWriteResponse. */ - class BatchWriteResponse implements IBatchWriteResponse { - - /** - * Constructs a new BatchWriteResponse. - * @param [properties] Properties to set - */ - constructor(properties?: google.firestore.v1.IBatchWriteResponse); - - /** BatchWriteResponse writeResults. */ - public writeResults: google.firestore.v1.IWriteResult[]; - - /** BatchWriteResponse status. */ - public status: google.rpc.IStatus[]; - } - /** Properties of a StructuredQuery. */ interface IStructuredQuery { diff --git a/dev/protos/firestore_v1_proto_api.js b/dev/protos/firestore_v1_proto_api.js index 5fd8fd789..860d68df5 100644 --- a/dev/protos/firestore_v1_proto_api.js +++ b/dev/protos/firestore_v1_proto_api.js @@ -3547,39 +3547,6 @@ $root.google = (function() { */ /** - * Callback as used by {@link google.firestore.v1.Firestore#batchWrite}. - * @memberof google.firestore.v1.Firestore - * @typedef BatchWriteCallback - * @type {function} - * @param {Error|null} error Error, if any - * @param {google.firestore.v1.BatchWriteResponse} [response] BatchWriteResponse - */ - - /** - * Calls BatchWrite. - * @function batchWrite - * @memberof google.firestore.v1.Firestore - * @instance - * @param {google.firestore.v1.IBatchWriteRequest} request BatchWriteRequest message or plain object - * @param {google.firestore.v1.Firestore.BatchWriteCallback} callback Node-style callback called with the error, if any, and BatchWriteResponse - * @returns {undefined} - * @variation 1 - */ - Object.defineProperty(Firestore.prototype.batchWrite = function batchWrite(request, callback) { - return this.rpcCall(batchWrite, $root.google.firestore.v1.BatchWriteRequest, $root.google.firestore.v1.BatchWriteResponse, request, callback); - }, "name", { value: "BatchWrite" }); - - /** - * Calls BatchWrite. - * @function batchWrite - * @memberof google.firestore.v1.Firestore - * @instance - * @param {google.firestore.v1.IBatchWriteRequest} request BatchWriteRequest message or plain object - * @returns {Promise} Promise - * @variation 2 - */ - - /* * Callback as used by {@link google.firestore.v1.Firestore#createDocument}. * @memberof google.firestore.v1.Firestore * @typedef CreateDocumentCallback @@ -5272,97 +5239,6 @@ $root.google = (function() { return ListCollectionIdsResponse; })(); - v1.BatchWriteRequest = (function() { - - /** - * Properties of a BatchWriteRequest. - * @memberof google.firestore.v1 - * @interface IBatchWriteRequest - * @property {string|null} [database] BatchWriteRequest database - * @property {Array.|null} [writes] BatchWriteRequest writes - */ - - /** - * Constructs a new BatchWriteRequest. - * @memberof google.firestore.v1 - * @classdesc Represents a BatchWriteRequest. - * @implements IBatchWriteRequest - * @constructor - * @param {google.firestore.v1.IBatchWriteRequest=} [properties] Properties to set - */ - function BatchWriteRequest(properties) { - this.writes = []; - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * BatchWriteRequest database. - * @member {string} database - * @memberof google.firestore.v1.BatchWriteRequest - * @instance - */ - BatchWriteRequest.prototype.database = ""; - - /** - * BatchWriteRequest writes. - * @member {Array.} writes - * @memberof google.firestore.v1.BatchWriteRequest - * @instance - */ - BatchWriteRequest.prototype.writes = $util.emptyArray; - - return BatchWriteRequest; - })(); - - v1.BatchWriteResponse = (function() { - - /** - * Properties of a BatchWriteResponse. - * @memberof google.firestore.v1 - * @interface IBatchWriteResponse - * @property {Array.|null} [writeResults] BatchWriteResponse writeResults - * @property {Array.|null} [status] BatchWriteResponse status - */ - - /** - * Constructs a new BatchWriteResponse. - * @memberof google.firestore.v1 - * @classdesc Represents a BatchWriteResponse. - * @implements IBatchWriteResponse - * @constructor - * @param {google.firestore.v1.IBatchWriteResponse=} [properties] Properties to set - */ - function BatchWriteResponse(properties) { - this.writeResults = []; - this.status = []; - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * BatchWriteResponse writeResults. - * @member {Array.} writeResults - * @memberof google.firestore.v1.BatchWriteResponse - * @instance - */ - BatchWriteResponse.prototype.writeResults = $util.emptyArray; - - /** - * BatchWriteResponse status. - * @member {Array.} status - * @memberof google.firestore.v1.BatchWriteResponse - * @instance - */ - BatchWriteResponse.prototype.status = $util.emptyArray; - - return BatchWriteResponse; - })(); - v1.StructuredQuery = (function() { /** diff --git a/dev/protos/google/firestore/v1/firestore.proto b/dev/protos/google/firestore/v1/firestore.proto index c2c374e9d..5f9b6d732 100644 --- a/dev/protos/google/firestore/v1/firestore.proto +++ b/dev/protos/google/firestore/v1/firestore.proto @@ -161,14 +161,6 @@ service Firestore { option (google.api.method_signature) = "parent"; } - // Commit a batch of non-transactional writes. - rpc BatchWrite(BatchWriteRequest) returns (BatchWriteResponse) { - option (google.api.http) = { - post: "/v1beta1/{database=projects/*/databases/*}/documents:batchWrite" - body: "*" - }; - } - // Creates a new document. rpc CreateDocument(CreateDocumentRequest) returns (Document) { option (google.api.http) = { @@ -764,33 +756,3 @@ message ListCollectionIdsResponse { // A page token that may be used to continue the list. string next_page_token = 2; } - -// The request for [Firestore.BatchWrite][]. -message BatchWriteRequest { - // The database name. In the format: - // `projects/{project_id}/databases/{database_id}`. - string database = 1; - // The writes to apply. - // The writes are not applied atomically, and can be applied out of order. - // More than 1 write per document is not allowed. - // The success status of each write is independent of the other ones (some may - // fail and some may succeed) and is specified in the BatchWriteResponse. - // Note that the writes here are not applied atomically but the writes in the - // Write(stream WriteRequest) are applied atomically and they cannot be - // batched. - repeated Write writes = 2; -} - -// The response from [Firestore.BatchWrite][]. -message BatchWriteResponse { - // The result of applying the writes. - // - // This i-th write result corresponds to the i-th write in the - // request. - repeated WriteResult write_results = 1; - // The status of applying the writes. - // - // This i-th write status corresponds to the i-th write in the - // request. - repeated google.rpc.Status status = 2; -} diff --git a/dev/src/backoff.ts b/dev/src/backoff.ts index 2aff2a9cc..f0bc32017 100644 --- a/dev/src/backoff.ts +++ b/dev/src/backoff.ts @@ -55,9 +55,9 @@ const DEFAULT_JITTER_FACTOR = 1.0; export const MAX_RETRY_ATTEMPTS = 10; /*! - * The timeout handler used by `ExponentialBackoff` and `BulkWriter`. + * The timeout handler used by `ExponentialBackoff`. */ -export let delayExecution: (f: () => void, ms: number) => void = setTimeout; +let delayExecution: (f: () => void, ms: number) => void = setTimeout; /** * Allows overriding of the timeout handler used by the exponential backoff diff --git a/dev/src/bulk-writer.ts b/dev/src/bulk-writer.ts deleted file mode 100644 index 7ef437620..000000000 --- a/dev/src/bulk-writer.ts +++ /dev/null @@ -1,657 +0,0 @@ -/*! - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as assert from 'assert'; - -import {FieldPath, Firestore} from '.'; -import {delayExecution} from './backoff'; -import {RateLimiter} from './rate-limiter'; -import {DocumentReference} from './reference'; -import {Timestamp} from './timestamp'; -import {Precondition, SetOptions, UpdateData} from './types'; -import {Deferred} from './util'; -import {BatchWriteResult, WriteBatch, WriteResult} from './write-batch'; - -/*! - * The maximum number of writes that can be in a single batch. - */ -const MAX_BATCH_SIZE = 500; - -/*! - * The starting maximum number of operations per second as allowed by the - * 500/50/5 rule. - * - * https://cloud.google.com/datastore/docs/best-practices#ramping_up_traffic. - */ -const STARTING_MAXIMUM_OPS_PER_SECOND = 500; - -/*! - * The rate by which to increase the capacity as specified by the 500/50/5 rule. - * - * https://cloud.google.com/datastore/docs/best-practices#ramping_up_traffic. - */ -const RATE_LIMITER_MULTIPLIER = 1.5; - -/*! - * How often the operations per second capacity should increase in milliseconds - * as specified by the 500/50/5 rule. - * - * https://cloud.google.com/datastore/docs/best-practices#ramping_up_traffic. - */ -const RATE_LIMITER_MULTIPLIER_MILLIS = 5 * 60 * 1000; - -/*! - * Used to represent the state of batch. - * - * Writes can only be added while the batch is OPEN. For a batch to be sent, - * the batch must be READY_TO_SEND. After a batch is sent, it is marked as SENT. - */ -enum BatchState { - OPEN, - READY_TO_SEND, - SENT, -} - -/** - * Used to represent a batch on the BatchQueue. - * - * @private - */ -class BulkCommitBatch { - /** - * The state of the batch. - */ - state = BatchState.OPEN; - - // The set of document reference paths present in the WriteBatch. - readonly docPaths = new Set(); - - // A deferred promise that is resolved after the batch has been sent, and a - // response is received. - private completedDeferred = new Deferred(); - - // A map from each WriteBatch operation to its corresponding result. - private resultsMap = new Map>(); - - constructor( - private readonly writeBatch: WriteBatch, - private readonly maxBatchSize: number - ) {} - - /** - * The number of writes in this batch. - */ - get opCount(): number { - return this.resultsMap.size; - } - - /** - * Adds a `create` operation to the WriteBatch. Returns a promise that - * resolves with the result of the write. - */ - create(documentRef: DocumentReference, data: T): Promise { - this.writeBatch.create(documentRef, data); - return this.processOperation(documentRef); - } - - /** - * Adds a `delete` operation to the WriteBatch. Returns a promise that - * resolves with the result of the delete. - */ - delete( - documentRef: DocumentReference, - precondition?: Precondition - ): Promise { - this.writeBatch.delete(documentRef, precondition); - return this.processOperation(documentRef); - } - - /** - * Adds a `set` operation to the WriteBatch. Returns a promise that - * resolves with the result of the write. - */ - set( - documentRef: DocumentReference, - data: T, - options?: SetOptions - ): Promise { - this.writeBatch.set(documentRef, data, options); - return this.processOperation(documentRef); - } - - /** - * Adds an `update` operation to the WriteBatch. Returns a promise that - * resolves with the result of the write. - */ - update( - documentRef: DocumentReference, - dataOrField: UpdateData | string | FieldPath, - ...preconditionOrValues: Array< - {lastUpdateTime?: Timestamp} | unknown | string | FieldPath - > - ): Promise { - this.writeBatch.update(documentRef, dataOrField, ...preconditionOrValues); - return this.processOperation(documentRef); - } - - /** - * Helper to update data structures associated with the operation and - * return the result. - */ - private processOperation( - documentRef: DocumentReference - ): Promise { - assert( - !this.docPaths.has(documentRef.path), - 'Batch should not contain writes to the same document' - ); - assert( - this.state === BatchState.OPEN, - 'Batch should be OPEN when adding writes' - ); - this.docPaths.add(documentRef.path); - const deferred = new Deferred(); - this.resultsMap.set(this.opCount, deferred); - - if (this.opCount === this.maxBatchSize) { - this.state = BatchState.READY_TO_SEND; - } - - return deferred.promise.then(result => { - if (result.writeTime) { - return new WriteResult(result.writeTime); - } else { - throw result.status; - } - }); - } - - /** - * Commits the batch and returns a promise that resolves with the result of - * all writes in this batch. - */ - bulkCommit(): Promise { - assert( - this.state === BatchState.READY_TO_SEND, - 'The batch should be marked as READY_TO_SEND before committing' - ); - this.state = BatchState.SENT; - return this.writeBatch.bulkCommit(); - } - - /** - * Resolves the individual operations in the batch with the results. - */ - processResults(results: BatchWriteResult[], error?: Error): void { - if (error === undefined) { - for (let i = 0; i < this.opCount; i++) { - this.resultsMap.get(i)!.resolve(results[i]); - } - } else { - for (let i = 0; i < this.opCount; i++) { - this.resultsMap.get(i)!.reject(error); - } - } - this.completedDeferred.resolve(); - } - - /** - * Returns a promise that resolves when the batch has been sent, and a - * response is received. - */ - awaitBulkCommit(): Promise { - this.markReadyToSend(); - return this.completedDeferred.promise; - } - - markReadyToSend(): void { - if (this.state === BatchState.OPEN) { - this.state = BatchState.READY_TO_SEND; - } - } -} - -/** - * A Firestore BulkWriter than can be used to perform a large number of writes - * in parallel. Writes to the same document will be executed sequentially. - * - * @class - * @private - */ -export class BulkWriter { - /** - * The maximum number of writes that can be in a single batch. - */ - private maxBatchSize = MAX_BATCH_SIZE; - - /** - * A queue of batches to be written. - */ - private batchQueue: BulkCommitBatch[] = []; - - /** - * Whether this BulkWriter instance is closed. Once closed, it cannot be - * opened again. - */ - private closed = false; - - /** - * Rate limiter used to throttle requests as per the 500/50/5 rule. - */ - private rateLimiter: RateLimiter; - - constructor( - private readonly firestore: Firestore, - enableThrottling: boolean - ) { - if (enableThrottling) { - this.rateLimiter = new RateLimiter( - STARTING_MAXIMUM_OPS_PER_SECOND, - RATE_LIMITER_MULTIPLIER, - RATE_LIMITER_MULTIPLIER_MILLIS - ); - } else { - this.rateLimiter = new RateLimiter( - Number.POSITIVE_INFINITY, - Number.POSITIVE_INFINITY, - Number.POSITIVE_INFINITY - ); - } - } - - /** - * Create a document with the provided data. This single operation will fail - * if a document exists at its location. - * - * @param {DocumentReference} documentRef A reference to the document to be - * created. - * @param {T} data The object to serialize as the document. - * @returns {Promise} A promise that resolves with the result of - * the write. Throws an error if the write fails. - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * let documentRef = firestore.collection('col').doc(); - * - * bulkWriter - * .create(documentRef, {foo: 'bar'}) - * .then(result => { - * console.log('Successfully executed write at: ', result); - * }) - * .catch(err => { - * console.log('Write failed with: ', err); - * }); - * }); - */ - create(documentRef: DocumentReference, data: T): Promise { - this.verifyNotClosed(); - const bulkCommitBatch = this.getEligibleBatch(documentRef); - const resultPromise = bulkCommitBatch.create(documentRef, data); - this.sendReadyBatches(); - return resultPromise; - } - - /** - * Delete a document from the database. - * - * @param {DocumentReference} documentRef A reference to the document to be - * deleted. - * @param {Precondition=} precondition A precondition to enforce for this - * delete. - * @param {Timestamp=} precondition.lastUpdateTime If set, enforces that the - * document was last updated at lastUpdateTime. Fails the batch if the - * document doesn't exist or was last updated at a different time. - * @returns {Promise} A promise that resolves with the result of - * the write. Throws an error if the write fails. - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * let documentRef = firestore.doc('col/doc'); - * - * bulkWriter - * .delete(documentRef) - * .then(result => { - * console.log('Successfully deleted document at: ', result); - * }) - * .catch(err => { - * console.log('Delete failed with: ', err); - * }); - * }); - */ - delete( - documentRef: DocumentReference, - precondition?: Precondition - ): Promise { - this.verifyNotClosed(); - const bulkCommitBatch = this.getEligibleBatch(documentRef); - const resultPromise = bulkCommitBatch.delete(documentRef, precondition); - this.sendReadyBatches(); - return resultPromise; - } - - /** - * Write to the document referred to by the provided - * [DocumentReference]{@link DocumentReference}. If the document does not - * exist yet, it will be created. If you pass [SetOptions]{@link SetOptions}., - * the provided data can be merged into the existing document. - * - * @param {DocumentReference} documentRef A reference to the document to be - * set. - * @param {T} data The object to serialize as the document. - * @param {SetOptions=} options An object to configure the set behavior. - * @param {boolean=} options.merge - If true, set() merges the values - * specified in its data argument. Fields omitted from this set() call remain - * untouched. - * @param {Array.=} options.mergeFields - If provided, set() - * only replaces the specified field paths. Any field path that is not - * specified is ignored and remains untouched. - * @returns {Promise} A promise that resolves with the result of - * the write. Throws an error if the write fails. - * - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * let documentRef = firestore.collection('col').doc(); - * - * bulkWriter - * .set(documentRef, {foo: 'bar'}) - * .then(result => { - * console.log('Successfully executed write at: ', result); - * }) - * .catch(err => { - * console.log('Write failed with: ', err); - * }); - * }); - */ - set( - documentRef: DocumentReference, - data: T, - options?: SetOptions - ): Promise { - this.verifyNotClosed(); - const bulkCommitBatch = this.getEligibleBatch(documentRef); - const resultPromise = bulkCommitBatch.set(documentRef, data, options); - this.sendReadyBatches(); - return resultPromise; - } - - /** - * Update fields of the document referred to by the provided - * [DocumentReference]{@link DocumentReference}. If the document doesn't yet - * exist, the update fails and the entire batch will be rejected. - * - * The update() method accepts either an object with field paths encoded as - * keys and field values encoded as values, or a variable number of arguments - * that alternate between field paths and field values. Nested fields can be - * updated by providing dot-separated field path strings or by providing - * FieldPath objects. - * - * - * A Precondition restricting this update can be specified as the last - * argument. - * - * @param {DocumentReference} documentRef A reference to the document to be - * updated. - * @param {UpdateData|string|FieldPath} dataOrField An object containing the - * fields and values with which to update the document or the path of the - * first field to update. - * @param {...(Precondition|*|string|FieldPath)} preconditionOrValues - An - * alternating list of field paths and values to update or a Precondition to - * restrict this update - * @returns {Promise} A promise that resolves with the result of - * the write. Throws an error if the write fails. - * - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * let documentRef = firestore.doc('col/doc'); - * - * bulkWriter - * .update(documentRef, {foo: 'bar'}) - * .then(result => { - * console.log('Successfully executed write at: ', result); - * }) - * .catch(err => { - * console.log('Write failed with: ', err); - * }); - * }); - */ - update( - documentRef: DocumentReference, - dataOrField: UpdateData | string | FieldPath, - ...preconditionOrValues: Array< - {lastUpdateTime?: Timestamp} | unknown | string | FieldPath - > - ): Promise { - this.verifyNotClosed(); - const bulkCommitBatch = this.getEligibleBatch(documentRef); - const resultPromise = bulkCommitBatch.update( - documentRef, - dataOrField, - ...preconditionOrValues - ); - this.sendReadyBatches(); - return resultPromise; - } - - /** - * Commits all writes that have been enqueued up to this point in parallel. - * - * Returns a Promise that resolves when all currently queued operations have - * been committed. The Promise will never be rejected since the results for - * each individual operation are conveyed via their individual Promises. - * - * The Promise resolves immediately if there are no pending writes. Otherwise, - * the Promise waits for all previously issued writes, but it does not wait - * for writes that were added after the method is called. If you want to wait - * for additional writes, call `flush()` again. - * - * @return {Promise} A promise that resolves when all enqueued writes - * up to this point have been committed. - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * - * bulkWriter.create(documentRef, {foo: 'bar'}); - * bulkWriter.update(documentRef2, {foo: 'bar'}); - * bulkWriter.delete(documentRef3); - * await flush().then(() => { - * console.log('Executed all writes'); - * }); - */ - async flush(): Promise { - this.verifyNotClosed(); - const trackedBatches = this.batchQueue; - const writePromises = trackedBatches.map(batch => batch.awaitBulkCommit()); - this.sendReadyBatches(); - await Promise.all(writePromises); - } - - /** - * Commits all enqueued writes and marks the BulkWriter instance as closed. - * - * After calling `close()`, calling any method wil throw an error. - * - * Returns a Promise that resolves when there are no more pending writes. The - * Promise will never be rejected. Calling this method will send all requests. - * The promise resolves immediately if there are no pending writes. - * - * @return {Promise} A promise that resolves when all enqueued writes - * up to this point have been committed. - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * - * bulkWriter.create(documentRef, {foo: 'bar'}); - * bulkWriter.update(documentRef2, {foo: 'bar'}); - * bulkWriter.delete(documentRef3); - * await close().then(() => { - * console.log('Executed all writes'); - * }); - */ - close(): Promise { - const flushPromise = this.flush(); - this.closed = true; - return flushPromise; - } - - private verifyNotClosed(): void { - if (this.closed) { - throw new Error('BulkWriter has already been closed.'); - } - } - - /** - * Return the first eligible batch that can hold a write to the provided - * reference, or creates one if no eligible batches are found. - * - * @private - */ - private getEligibleBatch(ref: DocumentReference): BulkCommitBatch { - if (this.batchQueue.length > 0) { - const lastBatch = this.batchQueue[this.batchQueue.length - 1]; - if ( - lastBatch.state === BatchState.OPEN && - !lastBatch.docPaths.has(ref.path) - ) { - return lastBatch; - } - } - return this.createNewBatch(); - } - - /** - * Creates a new batch and adds it to the BatchQueue. If there is already a - * batch enqueued, sends the batch after a new one is created. - * - * @private - */ - private createNewBatch(): BulkCommitBatch { - const newBatch = new BulkCommitBatch( - this.firestore.batch(), - this.maxBatchSize - ); - - if (this.batchQueue.length > 0) { - this.batchQueue[this.batchQueue.length - 1].markReadyToSend(); - this.sendReadyBatches(); - } - this.batchQueue.push(newBatch); - return newBatch; - } - - /** - * Attempts to send batches starting from the front of the BatchQueue until a - * batch cannot be sent. - * - * After a batch is complete, try sending batches again. - * - * @private - */ - private sendReadyBatches(): void { - const unsentBatches = this.batchQueue.filter( - batch => batch.state === BatchState.READY_TO_SEND - ); - - let index = 0; - while ( - index < unsentBatches.length && - this.isBatchSendable(unsentBatches[index]) - ) { - const batch = unsentBatches[index]; - - // Send the batch if it is under the rate limit, or schedule another - // attempt after the appropriate timeout. - const delayMs = this.rateLimiter.getNextRequestDelayMs(batch.opCount); - assert(delayMs !== -1, 'Batch size should be under capacity'); - if (delayMs === 0) { - this.sendBatch(batch); - } else { - delayExecution(() => this.sendReadyBatches(), delayMs); - break; - } - - index++; - } - } - - /** - * Sends the provided batch and processes the results. After the batch is - * committed, sends the next group of ready batches. - * - * @private - */ - private sendBatch(batch: BulkCommitBatch): void { - const success = this.rateLimiter.tryMakeRequest(batch.opCount); - assert(success, 'Batch should be under rate limit to be sent.'); - batch - .bulkCommit() - .then(results => { - batch.processResults(results); - }) - .catch((error: Error) => { - batch.processResults([], error); - }) - .then(() => { - // Remove the batch from the BatchQueue after it has been processed. - const batchIndex = this.batchQueue.indexOf(batch); - assert(batchIndex !== -1, 'The batch should be in the BatchQueue'); - this.batchQueue.splice(batchIndex, 1); - - this.sendReadyBatches(); - }); - } - - /** - * Checks that the provided batch is sendable. To be sendable, a batch must: - * (1) be marked as READY_TO_SEND - * (2) not write to references that are currently in flight - * - * @private - */ - private isBatchSendable(batch: BulkCommitBatch): boolean { - if (batch.state !== BatchState.READY_TO_SEND) { - return false; - } - - for (const path of batch.docPaths) { - const isRefInFlight = - this.batchQueue - .filter(batch => batch.state === BatchState.SENT) - .find(batch => batch.docPaths.has(path)) !== undefined; - if (isRefInFlight) { - console.warn( - '[BulkWriter]', - `Duplicate write to document "${path}" detected.`, - 'Writing to the same document multiple times will slow down BulkWriter. ' + - 'Write to unique documents in order to maximize throughput.' - ); - return false; - } - } - - return true; - } - - /** - * Sets the maximum number of allowed operations in a batch. - * - * @private - */ - // Visible for testing. - _setMaxBatchSize(size: number): void { - this.maxBatchSize = size; - } -} diff --git a/dev/src/document.ts b/dev/src/document.ts index 6ce7ccb99..55ffbad44 100644 --- a/dev/src/document.ts +++ b/dev/src/document.ts @@ -452,6 +452,16 @@ export class DocumentSnapshot { return (fields as ApiMapValue)[components[0]]; } + /** + * Checks whether this DocumentSnapshot contains any fields. + * + * @private + * @return {boolean} + */ + get isEmpty(): boolean { + return this._fieldsProto === undefined || isEmpty(this._fieldsProto); + } + /** * Convert a document snapshot to the Firestore 'Document' Protobuf. * @@ -941,16 +951,29 @@ export class DocumentTransform { } /** - * Converts a document transform to the Firestore 'FieldTransform' Proto. + * Converts a document transform to the Firestore 'DocumentTransform' Proto. * * @private * @param serializer The Firestore serializer - * @returns A list of Firestore 'FieldTransform' Protos + * @returns A Firestore 'DocumentTransform' Proto or 'null' if this transform + * is empty. */ - toProto(serializer: Serializer): api.DocumentTransform.IFieldTransform[] { - return Array.from(this.transforms, ([path, transform]) => - transform.toProto(serializer, path) - ); + toProto(serializer: Serializer): api.IWrite | null { + if (this.isEmpty) { + return null; + } + + const fieldTransforms: api.DocumentTransform.IFieldTransform[] = []; + for (const [path, transform] of this.transforms) { + fieldTransforms.push(transform.toProto(serializer, path)); + } + + return { + transform: { + document: this.ref.formattedName, + fieldTransforms, + }, + }; } } diff --git a/dev/src/index.ts b/dev/src/index.ts index a51afdd0a..eb2c98e80 100644 --- a/dev/src/index.ts +++ b/dev/src/index.ts @@ -21,7 +21,6 @@ import {URL} from 'url'; import {google} from '../protos/firestore_v1_proto_api'; import {ExponentialBackoff, ExponentialBackoffSetting} from './backoff'; -import {BulkWriter} from './bulk-writer'; import {fieldsFromJson, timestampFromJson} from './convert'; import { DocumentSnapshot, @@ -44,7 +43,6 @@ import {Timestamp} from './timestamp'; import {parseGetAllArguments, Transaction} from './transaction'; import { ApiMapValue, - BulkWriterOptions, DocumentData, FirestoreStreamingMethod, FirestoreUnaryMethod, @@ -86,7 +84,6 @@ export {FieldPath} from './path'; export {GeoPoint} from './geo-point'; export {setLogFunction} from './logger'; export { - BulkWriterOptions, FirestoreDataConverter, UpdateData, DocumentData, @@ -643,44 +640,6 @@ export class Firestore { return new WriteBatch(this); } - /** - * Creates a [BulkWriter]{@link BulkWriter}, used for performing - * multiple writes in parallel. Gradually ramps up writes as specified - * by the 500/50/5 rule. - * - * @see [500/50/5 Documentation]{@link https://cloud.google.com/datastore/docs/best-practices#ramping_up_traffic} - * - * @private - * @param {object=} options BulkWriter options. - * @param {boolean=} options.disableThrottling Whether to disable throttling - * as specified by the 500/50/5 rule. - * @returns {WriteBatch} A BulkWriter that operates on this Firestore - * client. - * - * @example - * let bulkWriter = firestore.bulkWriter(); - * - * bulkWriter.create(firestore.doc('col/doc1'), {foo: 'bar'}) - * .then(res => { - * console.log(`Added document at ${res.writeTime}`); - * }); - * bulkWriter.update(firestore.doc('col/doc2'), {foo: 'bar'}) - * .then(res => { - * console.log(`Updated document at ${res.writeTime}`); - * }); - * bulkWriter.delete(firestore.doc('col/doc3')) - * .then(res => { - * console.log(`Deleted document at ${res.writeTime}`); - * }); - * await bulkWriter.flush().then(() => { - * console.log('Executed all writes'); - * }); - * bulkWriter.close(); - */ - _bulkWriter(options?: BulkWriterOptions): BulkWriter { - return new BulkWriter(this, !options?.disableThrottling); - } - /** * Creates a [DocumentSnapshot]{@link DocumentSnapshot} or a * [QueryDocumentSnapshot]{@link QueryDocumentSnapshot} from a diff --git a/dev/src/rate-limiter.ts b/dev/src/rate-limiter.ts deleted file mode 100644 index c3ee91d33..000000000 --- a/dev/src/rate-limiter.ts +++ /dev/null @@ -1,154 +0,0 @@ -/*! - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as assert from 'assert'; - -import {Timestamp} from './timestamp'; - -/** - * A helper that uses the Token Bucket algorithm to rate limit the number of - * operations that can be made in a second. - * - * Before a given request containing a number of operations can proceed, - * RateLimiter determines doing so stays under the provided rate limits. It can - * also determine how much time is required before a request can be made. - * - * RateLimiter can also implement a gradually increasing rate limit. This is - * used to enforce the 500/50/5 rule - * (https://cloud.google.com/datastore/docs/best-practices#ramping_up_traffic). - * - * @private - */ -export class RateLimiter { - // Number of tokens available. Each operation consumes one token. - availableTokens: number; - - // When the token bucket was last refilled. - lastRefillTimeMillis: number; - - /** - * @param initialCapacity Initial maximum number of operations per second. - * @param multiplier Rate by which to increase the capacity. - * @param multiplierMillis How often the capacity should increase in - * milliseconds. - * @param startTimeMillis The starting time in epoch milliseconds that the - * rate limit is based on. Used for testing the limiter. - */ - constructor( - private readonly initialCapacity: number, - private readonly multiplier: number, - private readonly multiplierMillis: number, - private readonly startTimeMillis = Date.now() - ) { - this.availableTokens = initialCapacity; - this.lastRefillTimeMillis = startTimeMillis; - } - - /** - * Tries to make the number of operations. Returns true if the request - * succeeded and false otherwise. - * - * @param requestTimeMillis The date used to calculate the number of available - * tokens. Used for testing the limiter. - * @private - */ - tryMakeRequest( - numOperations: number, - requestTimeMillis = Date.now() - ): boolean { - this.refillTokens(requestTimeMillis); - if (numOperations <= this.availableTokens) { - this.availableTokens -= numOperations; - return true; - } - return false; - } - - /** - * Returns the number of ms needed to make a request with the provided number - * of operations. Returns 0 if the request can be made with the existing - * capacity. Returns -1 if the request is not possible with the current - * capacity. - * - * @param requestTimeMillis The date used to calculate the number of available - * tokens. Used for testing the limiter. - * @private - */ - getNextRequestDelayMs( - numOperations: number, - requestTimeMillis = Date.now() - ): number { - this.refillTokens(requestTimeMillis); - if (numOperations < this.availableTokens) { - return 0; - } - - const capacity = this.calculateCapacity(requestTimeMillis); - if (capacity < numOperations) { - return -1; - } - - const requiredTokens = numOperations - this.availableTokens; - return Math.ceil((requiredTokens * 1000) / capacity); - } - - /** - * Refills the number of available tokens based on how much time has elapsed - * since the last time the tokens were refilled. - * - * @param requestTimeMillis The date used to calculate the number of available - * tokens. Used for testing the limiter. - * @private - */ - private refillTokens(requestTimeMillis = Date.now()): void { - if (requestTimeMillis >= this.lastRefillTimeMillis) { - const elapsedTime = requestTimeMillis - this.lastRefillTimeMillis; - const capacity = this.calculateCapacity(requestTimeMillis); - const tokensToAdd = Math.floor((elapsedTime * capacity) / 1000); - if (tokensToAdd > 0) { - this.availableTokens = Math.min( - capacity, - this.availableTokens + tokensToAdd - ); - this.lastRefillTimeMillis = requestTimeMillis; - } - } else { - throw new Error( - 'Request time should not be before the last token refill time.' - ); - } - } - - /** - * Calculates the maximum capacity based on the provided date. - * - * @private - */ - // Visible for testing. - calculateCapacity(requestTimeMillis: number): number { - assert( - requestTimeMillis >= this.startTimeMillis, - 'startTime cannot be after currentTime' - ); - const millisElapsed = requestTimeMillis - this.startTimeMillis; - const operationsPerSecond = Math.floor( - Math.pow( - this.multiplier, - Math.floor(millisElapsed / this.multiplierMillis) - ) * this.initialCapacity - ); - return operationsPerSecond; - } -} diff --git a/dev/src/types.ts b/dev/src/types.ts index f43415821..d318074b4 100644 --- a/dev/src/types.ts +++ b/dev/src/types.ts @@ -46,10 +46,6 @@ export interface GapicClient { request: api.ICommitRequest, options?: CallOptions ): Promise<[api.ICommitResponse, unknown, unknown]>; - batchWrite( - request: api.IBatchWriteRequest, - options?: CallOptions - ): Promise<[api.IBatchWriteResponse, unknown, unknown]>; rollback( request: api.IRollbackRequest, options?: CallOptions @@ -77,8 +73,7 @@ export type FirestoreUnaryMethod = | 'listCollectionIds' | 'rollback' | 'beginTransaction' - | 'commit' - | 'batchWrite'; + | 'commit'; /** Streaming methods used in the Firestore SDK. */ export type FirestoreStreamingMethod = @@ -321,15 +316,6 @@ export interface ValidationOptions { allowTransforms: boolean; } -/** - * An options object that can be used to disable request throttling in - * BulkWriter. - */ -export interface BulkWriterOptions { - /** Whether to disable throttling. */ - readonly disableThrottling?: boolean; -} - /** * A Firestore Proto value in ProtoJs format. * @private diff --git a/dev/src/write-batch.ts b/dev/src/write-batch.ts index 20aba0bea..9c296e78a 100644 --- a/dev/src/write-batch.ts +++ b/dev/src/write-batch.ts @@ -48,7 +48,6 @@ import { } from './validate'; import api = google.firestore.v1; -import {GoogleError, Status} from 'google-gax'; /*! * Google Cloud Functions terminates idle connections after two minutes. After @@ -104,23 +103,11 @@ export class WriteResult { } } -/** - * A BatchWriteResult wraps the write time and status returned by Firestore - * when making BatchWriteRequests. - * - * @private - */ -export class BatchWriteResult { - constructor( - readonly writeTime: Timestamp | null, - readonly status: GoogleError - ) {} -} - /** Helper type to manage the list of writes in a WriteBatch. */ // TODO(mrschmidt): Replace with api.IWrite interface WriteOp { - write: api.IWrite; + write?: api.IWrite | null; + transform?: api.IWrite | null; precondition?: api.IPrecondition | null; } @@ -207,13 +194,12 @@ export class WriteBatch { const op = () => { const document = DocumentSnapshot.fromObject(documentRef, firestoreData); - const write = document.toProto(); - if (!transform.isEmpty) { - write.updateTransforms = transform.toProto(this._serializer); - } + const write = + !document.isEmpty || transform.isEmpty ? document.toProto() : null; return { write, + transform: transform.toProto(this._serializer), precondition: precondition.toProto(), }; }; @@ -334,21 +320,24 @@ export class WriteBatch { if (mergePaths) { documentMask!.removeFields(transform.fields); - } else if (mergeLeaves) { + } else { documentMask = DocumentMask.fromObject(firestoreData); } - const write = document.toProto(); - if (!transform.isEmpty) { - write.updateTransforms = transform.toProto(this._serializer); - } + const hasDocumentData = !document.isEmpty || !documentMask!.isEmpty; + + let write; - if (mergePaths || mergeLeaves) { + if (!mergePaths && !mergeLeaves) { + write = document.toProto(); + } else if (hasDocumentData || transform.isEmpty) { + write = document.toProto()!; write.updateMask = documentMask!.toProto(); } return { write, + transform: transform.toProto(this._serializer), }; }; @@ -485,13 +474,16 @@ export class WriteBatch { const op = () => { const document = DocumentSnapshot.fromUpdateMap(documentRef, updateMap); - const write = document.toProto(); - write!.updateMask = documentMask.toProto(); - if (!transform.isEmpty) { - write!.updateTransforms = transform.toProto(this._serializer); + let write: api.IWrite | null = null; + + if (!document.isEmpty || !documentMask.isEmpty) { + write = document.toProto(); + write!.updateMask = documentMask.toProto(); } + return { write, + transform: transform.toProto(this._serializer), precondition: precondition.toProto(), }; }; @@ -522,47 +514,6 @@ export class WriteBatch { return this.commit_(); } - /** - * Commits all pending operations to the database and verifies all - * preconditions. - * - * The writes in the batch are not applied atomically and can be applied out - * of order. - * - * @private - */ - async bulkCommit(): Promise { - this._committed = true; - const tag = requestTag(); - await this._firestore.initializeIfNeeded(tag); - - const database = this._firestore.formattedName; - const request: api.IBatchWriteRequest = {database, writes: []}; - const writes = this._ops.map(op => op()); - - for (const req of writes) { - if (req.precondition) { - req.write!.currentDocument = req.precondition; - } - request.writes!.push(req.write); - } - - const response = await this._firestore.request< - api.IBatchWriteRequest, - api.BatchWriteResponse - >('batchWrite', request, tag); - - return (response.writeResults || []).map((result, i) => { - const status = response.status[i]; - const error = new GoogleError(status.message || undefined); - error.code = status.code as Status; - return new BatchWriteResult( - result.updateTime ? Timestamp.fromProto(result.updateTime) : null, - error - ); - }); - } - /** * Commit method that takes an optional transaction ID. * @@ -601,38 +552,85 @@ export class WriteBatch { }); } - const request: api.ICommitRequest = {database, writes: []}; + const request: api.ICommitRequest = {database}; const writes = this._ops.map(op => op()); + request.writes = []; for (const req of writes) { + assert( + req.write || req.transform, + 'Either a write or transform must be set' + ); + if (req.precondition) { - req.write!.currentDocument = req.precondition; + (req.write || req.transform)!.currentDocument = req.precondition; + } + + if (req.write) { + request.writes.push(req.write); } - request.writes!.push(req.write); + if (req.transform) { + request.writes.push(req.transform); + } } logger( 'WriteBatch.commit', tag, 'Sending %d writes', - request.writes!.length + request.writes.length ); if (explicitTransaction) { request.transaction = explicitTransaction; } - const response = await this._firestore.request< - api.ICommitRequest, - api.CommitResponse - >('commit', request, tag); - - return (response.writeResults || []).map( - writeResult => - new WriteResult( - Timestamp.fromProto(writeResult.updateTime || response.commitTime!) - ) - ); + + return this._firestore + .request('commit', request, tag) + .then(resp => { + const writeResults: WriteResult[] = []; + + if (request.writes!.length > 0) { + assert( + Array.isArray(resp.writeResults) && + request.writes!.length === resp.writeResults.length, + `Expected one write result per operation, but got ${ + resp.writeResults.length + } results for ${request.writes!.length} operations.` + ); + + const commitTime = Timestamp.fromProto(resp.commitTime!); + + let offset = 0; + + for (let i = 0; i < writes.length; ++i) { + const writeRequest = writes[i]; + + // Don't return two write results for a write that contains a + // transform, as the fact that we have to split one write + // operation into two distinct write requests is an implementation + // detail. + if (writeRequest.write && writeRequest.transform) { + // The document transform is always sent last and produces the + // latest update time. + ++offset; + } + + const writeResult = resp.writeResults[i + offset]; + + writeResults.push( + new WriteResult( + writeResult.updateTime + ? Timestamp.fromProto(writeResult.updateTime) + : commitTime + ) + ); + } + } + + return writeResults; + }); } /** diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 37b2b6b9d..2c7804e48 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -419,21 +419,6 @@ describe('DocumentReference class', () => { }); }); - it('supports increment() with set() with merge', () => { - const baseData = {sum: 1}; - const updateData = {sum: FieldValue.increment(1)}; - const expectedData = {sum: 2}; - - const ref = randomCol.doc('doc'); - return ref - .set(baseData) - .then(() => ref.set(updateData, {merge: true})) - .then(() => ref.get()) - .then(doc => { - expect(doc.data()).to.deep.equal(expectedData); - }); - }); - it('supports arrayUnion()', () => { const baseObject = { a: [], @@ -2006,17 +1991,11 @@ describe('Transaction class', () => { it('has update() method', () => { const ref = randomCol.doc('doc'); return ref - .set({ - boo: ['ghost', 'sebastian'], - moo: 'chicken', - }) + .set({foo: 'bar'}) .then(() => { return firestore.runTransaction(updateFunction => { return updateFunction.get(ref).then(() => { - updateFunction.update(ref, { - boo: FieldValue.arrayRemove('sebastian'), - moo: 'cow', - }); + updateFunction.update(ref, {foo: 'foobar'}); }); }); }) @@ -2024,10 +2003,7 @@ describe('Transaction class', () => { return ref.get(); }) .then(doc => { - expect(doc.data()).to.deep.equal({ - boo: ['ghost'], - moo: 'cow', - }); + expect(doc.get('foo')).to.equal('foobar'); }); }); diff --git a/dev/test/bulk-writer.ts b/dev/test/bulk-writer.ts deleted file mode 100644 index 1366ff269..000000000 --- a/dev/test/bulk-writer.ts +++ /dev/null @@ -1,660 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {expect} from 'chai'; -import {Status} from 'google-gax'; - -import * as proto from '../protos/firestore_v1_proto_api'; -import { - DocumentData, - Firestore, - setLogFunction, - Timestamp, - WriteResult, -} from '../src'; -import {BulkWriter} from '../src/bulk-writer'; -import {Deferred} from '../src/util'; -import { - ApiOverride, - create, - createInstance, - document, - remove, - response, - set, - update, - updateMask, - verifyInstance, -} from './util/helpers'; - -import api = proto.google.firestore.v1; -import {setTimeoutHandler} from '../src/backoff'; - -// Change the argument to 'console.log' to enable debug output. -setLogFunction(() => {}); - -const PROJECT_ID = 'test-project'; - -interface RequestResponse { - request: api.IBatchWriteRequest; - response: api.IBatchWriteResponse; -} - -describe('BulkWriter', () => { - let firestore: Firestore; - let requestCounter: number; - let opCount: number; - const activeRequestDeferred = new Deferred(); - let activeRequestCounter = 0; - - beforeEach(() => { - requestCounter = 0; - opCount = 0; - }); - - function incrementOpCount(): void { - opCount++; - } - - function verifyOpCount(expected: number): void { - expect(opCount).to.equal(expected); - } - - function setOp(doc: string, value: string): api.IWrite { - return set({ - document: document(doc, 'foo', value), - }).writes![0]; - } - - function updateOp(doc: string, value: string): api.IWrite { - return update({ - document: document(doc, 'foo', value), - mask: updateMask('foo'), - }).writes![0]; - } - - function createOp(doc: string, value: string): api.IWrite { - return create({ - document: document(doc, 'foo', value), - }).writes![0]; - } - - function deleteOp(doc: string): api.IWrite { - return remove(doc).writes![0]; - } - - function createRequest(requests: api.IWrite[]): api.IBatchWriteRequest { - return { - writes: requests, - }; - } - - function successResponse(seconds: number): api.IBatchWriteResponse { - return { - writeResults: [ - { - updateTime: { - nanos: 0, - seconds, - }, - }, - ], - status: [{code: Status.OK}], - }; - } - - function failResponse(): api.IBatchWriteResponse { - return { - writeResults: [ - { - updateTime: null, - }, - ], - status: [{code: Status.UNAVAILABLE}], - }; - } - - function mergeResponses( - responses: api.IBatchWriteResponse[] - ): api.IBatchWriteResponse { - return { - writeResults: responses.map(v => v.writeResults![0]), - status: responses.map(v => v.status![0]), - }; - } - - /** - * Creates an instance with the mocked objects. - * - * @param enforceSingleConcurrentRequest Whether to check that there is only - * one active request at a time. If true, the `activeRequestDeferred` must be - * manually resolved for the response to return. - */ - function instantiateInstance( - mock: RequestResponse[], - enforceSingleConcurrentRequest = false - ): Promise { - const overrides: ApiOverride = { - batchWrite: async request => { - expect(request).to.deep.eq({ - database: `projects/${PROJECT_ID}/databases/(default)`, - writes: mock[requestCounter].request.writes, - }); - if (enforceSingleConcurrentRequest) { - activeRequestCounter++; - - // This expect statement is used to test that only one request is - // made at a time. - expect(activeRequestCounter).to.equal(1); - await activeRequestDeferred.promise; - activeRequestCounter--; - } - - const responsePromise = response({ - writeResults: mock[requestCounter].response.writeResults, - status: mock[requestCounter].response.status, - }); - requestCounter++; - return responsePromise; - }, - }; - return createInstance(overrides).then(firestoreClient => { - firestore = firestoreClient; - return firestore._bulkWriter(); - }); - } - - afterEach(() => verifyInstance(firestore)); - - it('has a set() method', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([setOp('doc', 'bar')]), - response: successResponse(2), - }, - ]); - const doc = firestore.doc('collectionId/doc'); - let writeResult: WriteResult; - bulkWriter.set(doc, {foo: 'bar'}).then(result => { - incrementOpCount(); - writeResult = result; - }); - return bulkWriter.close().then(async () => { - verifyOpCount(1); - expect(writeResult.writeTime.isEqual(new Timestamp(2, 0))).to.be.true; - }); - }); - - it('has an update() method', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([updateOp('doc', 'bar')]), - response: successResponse(2), - }, - ]); - const doc = firestore.doc('collectionId/doc'); - let writeResult: WriteResult; - bulkWriter.update(doc, {foo: 'bar'}).then(result => { - incrementOpCount(); - writeResult = result; - }); - return bulkWriter.close().then(async () => { - verifyOpCount(1); - expect(writeResult.writeTime.isEqual(new Timestamp(2, 0))).to.be.true; - }); - }); - - it('has a delete() method', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([deleteOp('doc')]), - response: successResponse(2), - }, - ]); - const doc = firestore.doc('collectionId/doc'); - let writeResult: WriteResult; - bulkWriter.delete(doc).then(result => { - incrementOpCount(); - writeResult = result; - }); - return bulkWriter.close().then(async () => { - verifyOpCount(1); - expect(writeResult.writeTime.isEqual(new Timestamp(2, 0))).to.be.true; - }); - }); - - it('has a create() method', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([createOp('doc', 'bar')]), - response: successResponse(2), - }, - ]); - const doc = firestore.doc('collectionId/doc'); - let writeResult: WriteResult; - bulkWriter.create(doc, {foo: 'bar'}).then(result => { - incrementOpCount(); - writeResult = result; - }); - return bulkWriter.close().then(async () => { - verifyOpCount(1); - expect(writeResult.writeTime.isEqual(new Timestamp(2, 0))).to.be.true; - }); - }); - - it('surfaces errors', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([setOp('doc', 'bar')]), - response: failResponse(), - }, - ]); - - const doc = firestore.doc('collectionId/doc'); - bulkWriter.set(doc, {foo: 'bar'}).catch(err => { - incrementOpCount(); - expect(err.code).to.equal(Status.UNAVAILABLE); - }); - - return bulkWriter.close().then(async () => verifyOpCount(1)); - }); - - it('flush() resolves immediately if there are no writes', async () => { - const bulkWriter = await instantiateInstance([]); - return bulkWriter.flush().then(() => verifyOpCount(0)); - }); - - it('adds writes to a new batch after calling flush()', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([createOp('doc', 'bar')]), - response: successResponse(2), - }, - { - request: createRequest([setOp('doc2', 'bar1')]), - response: successResponse(2), - }, - ]); - bulkWriter - .create(firestore.doc('collectionId/doc'), {foo: 'bar'}) - .then(incrementOpCount); - bulkWriter.flush(); - bulkWriter - .set(firestore.doc('collectionId/doc2'), {foo: 'bar1'}) - .then(incrementOpCount); - await bulkWriter.close().then(async () => { - verifyOpCount(2); - }); - }); - - it('close() sends all writes', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([createOp('doc', 'bar')]), - response: successResponse(2), - }, - ]); - const doc = firestore.doc('collectionId/doc'); - bulkWriter.create(doc, {foo: 'bar'}).then(incrementOpCount); - return bulkWriter.close().then(async () => { - verifyOpCount(1); - }); - }); - - it('close() resolves immediately if there are no writes', async () => { - const bulkWriter = await instantiateInstance([]); - return bulkWriter.close().then(() => verifyOpCount(0)); - }); - - it('cannot call methods after close() is called', async () => { - const bulkWriter = await instantiateInstance([]); - - const expected = 'BulkWriter has already been closed.'; - const doc = firestore.doc('collectionId/doc'); - await bulkWriter.close(); - expect(() => bulkWriter.set(doc, {})).to.throw(expected); - expect(() => bulkWriter.create(doc, {})).to.throw(expected); - expect(() => bulkWriter.update(doc, {})).to.throw(expected); - expect(() => bulkWriter.delete(doc)).to.throw(expected); - expect(bulkWriter.flush()).to.eventually.be.rejectedWith(expected); - expect(bulkWriter.close()).to.eventually.be.rejectedWith(expected); - }); - - it('sends writes to the same document in separate batches', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([setOp('doc', 'bar')]), - response: successResponse(0), - }, - { - request: createRequest([updateOp('doc', 'bar1')]), - response: successResponse(1), - }, - ]); - - // Create two document references pointing to the same document. - const doc = firestore.doc('collectionId/doc'); - const doc2 = firestore.doc('collectionId/doc'); - bulkWriter.set(doc, {foo: 'bar'}).then(incrementOpCount); - bulkWriter.update(doc2, {foo: 'bar1'}).then(incrementOpCount); - - return bulkWriter.close().then(async () => { - verifyOpCount(2); - }); - }); - - it('sends writes to different documents in the same batch', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([setOp('doc1', 'bar'), updateOp('doc2', 'bar')]), - response: mergeResponses([successResponse(0), successResponse(1)]), - }, - ]); - - const doc1 = firestore.doc('collectionId/doc1'); - const doc2 = firestore.doc('collectionId/doc2'); - bulkWriter.set(doc1, {foo: 'bar'}).then(incrementOpCount); - bulkWriter.update(doc2, {foo: 'bar'}).then(incrementOpCount); - - return bulkWriter.close().then(async () => { - verifyOpCount(2); - }); - }); - - it('splits into multiple batches after exceeding maximum batch size', async () => { - const arrayRange = Array.from(new Array(6), (_, i) => i); - const requests = arrayRange.map(i => setOp('doc' + i, 'bar')); - const responses = arrayRange.map(i => successResponse(i)); - const bulkWriter = await instantiateInstance([ - { - request: createRequest([requests[0], requests[1]]), - response: mergeResponses([responses[0], responses[1]]), - }, - { - request: createRequest([requests[2], requests[3]]), - response: mergeResponses([responses[2], responses[3]]), - }, - { - request: createRequest([requests[4], requests[5]]), - response: mergeResponses([responses[4], responses[5]]), - }, - ]); - - bulkWriter._setMaxBatchSize(2); - for (let i = 0; i < 6; i++) { - bulkWriter - .set(firestore.doc('collectionId/doc' + i), {foo: 'bar'}) - .then(incrementOpCount); - } - - return bulkWriter.close().then(async () => { - verifyOpCount(6); - }); - }); - - it('sends existing batches when a new batch is created', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([setOp('doc', 'bar')]), - response: successResponse(0), - }, - { - request: createRequest([ - updateOp('doc', 'bar1'), - createOp('doc2', 'bar1'), - ]), - response: mergeResponses([successResponse(1), successResponse(2)]), - }, - ]); - - bulkWriter._setMaxBatchSize(2); - - const doc = firestore.doc('collectionId/doc'); - const doc2 = firestore.doc('collectionId/doc2'); - - // Create a new batch by writing to the same document. - const setPromise = bulkWriter.set(doc, {foo: 'bar'}).then(incrementOpCount); - const updatePromise = bulkWriter - .update(doc, {foo: 'bar1'}) - .then(incrementOpCount); - await setPromise; - - // Create a new batch by reaching the batch size limit. - const createPromise = bulkWriter - .create(doc2, {foo: 'bar1'}) - .then(incrementOpCount); - - await updatePromise; - await createPromise; - verifyOpCount(3); - return bulkWriter.close(); - }); - - it('sends batches automatically when the batch size limit is reached', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([ - setOp('doc1', 'bar'), - updateOp('doc2', 'bar'), - createOp('doc3', 'bar'), - ]), - response: mergeResponses([ - successResponse(0), - successResponse(1), - successResponse(2), - ]), - }, - { - request: createRequest([deleteOp('doc4')]), - response: successResponse(3), - }, - ]); - - bulkWriter._setMaxBatchSize(3); - const promise1 = bulkWriter - .set(firestore.doc('collectionId/doc1'), {foo: 'bar'}) - .then(incrementOpCount); - const promise2 = bulkWriter - .update(firestore.doc('collectionId/doc2'), {foo: 'bar'}) - .then(incrementOpCount); - const promise3 = bulkWriter - .create(firestore.doc('collectionId/doc3'), {foo: 'bar'}) - .then(incrementOpCount); - - // The 4th write should not sent because it should be in a new batch. - bulkWriter - .delete(firestore.doc('collectionId/doc4')) - .then(incrementOpCount); - - await Promise.all([promise1, promise2, promise3]).then(() => { - verifyOpCount(3); - }); - - return bulkWriter.close().then(async () => { - verifyOpCount(4); - }); - }); - - it('does not send batches if a document containing the same write is in flight', async () => { - const bulkWriter = await instantiateInstance( - [ - { - request: createRequest([setOp('doc1', 'bar'), setOp('doc2', 'bar')]), - response: mergeResponses([successResponse(1), successResponse(2)]), - }, - { - request: createRequest([setOp('doc1', 'bar')]), - response: successResponse(3), - }, - ], - /* enforceSingleConcurrentRequest= */ true - ); - bulkWriter.set(firestore.doc('collectionId/doc1'), {foo: 'bar'}); - bulkWriter.set(firestore.doc('collectionId/doc2'), {foo: 'bar'}); - const flush1 = bulkWriter.flush(); - // The third write will be placed in a new batch - bulkWriter.set(firestore.doc('collectionId/doc1'), {foo: 'bar'}); - const flush2 = bulkWriter.flush(); - activeRequestDeferred.resolve(); - await flush1; - await flush2; - return bulkWriter.close(); - }); - - it('supports different type converters', async () => { - const bulkWriter = await instantiateInstance([ - { - request: createRequest([setOp('doc1', 'boo'), setOp('doc2', 'moo')]), - response: mergeResponses([successResponse(1), successResponse(2)]), - }, - ]); - - class Boo {} - const booConverter = { - toFirestore(): DocumentData { - return {foo: 'boo'}; - }, - fromFirestore(): Boo { - return new Boo(); - }, - }; - - class Moo {} - const mooConverter = { - toFirestore(): DocumentData { - return {foo: 'moo'}; - }, - fromFirestore(): Moo { - return new Moo(); - }, - }; - - const doc1 = firestore.doc('collectionId/doc1').withConverter(booConverter); - const doc2 = firestore.doc('collectionId/doc2').withConverter(mooConverter); - bulkWriter.set(doc1, new Boo()).then(incrementOpCount); - bulkWriter.set(doc2, new Moo()).then(incrementOpCount); - return bulkWriter.close().then(() => verifyOpCount(2)); - }); - - describe('500/50/5 support', () => { - afterEach(() => setTimeoutHandler(setTimeout)); - - it('does not send batches if doing so exceeds the rate limit', done => { - // The test is considered a success if BulkWriter tries to send the second - // batch again after a timeout. - - const arrayRange = Array.from(new Array(500), (_, i) => i); - const requests1 = arrayRange.map(i => setOp('doc' + i, 'bar')); - const responses1 = arrayRange.map(i => successResponse(i)); - const arrayRange2 = [500, 501, 502, 503, 504]; - const requests2 = arrayRange2.map(i => setOp('doc' + i, 'bar')); - const responses2 = arrayRange2.map(i => successResponse(i)); - - instantiateInstance([ - { - request: createRequest(requests1), - response: mergeResponses(responses1), - }, - { - request: createRequest(requests2), - response: mergeResponses(responses2), - }, - ]).then(bulkWriter => { - setTimeoutHandler(() => - done(new Error('This batch should not have a timeout')) - ); - for (let i = 0; i < 500; i++) { - bulkWriter - .set(firestore.doc('collectionId/doc' + i), {foo: 'bar'}) - .then(incrementOpCount); - } - bulkWriter.flush(); - - // Sending this next batch would go over the 500/50/5 capacity, so - // check that BulkWriter doesn't send this batch until the first batch - // is resolved. - setTimeoutHandler((_, timeout) => { - // Check that BulkWriter has not yet sent the 2nd batch. - expect(requestCounter).to.equal(0); - expect(timeout).to.be.greaterThan(0); - done(); - }); - for (let i = 500; i < 505; i++) { - bulkWriter - .set(firestore.doc('collectionId/doc' + i), {foo: 'bar'}) - .then(incrementOpCount); - } - return bulkWriter.flush(); - }); - }); - }); - - describe('if bulkCommit() fails', async () => { - function instantiateInstance(): Promise { - const overrides: ApiOverride = { - batchWrite: () => { - throw new Error('Mock batchWrite failed in test'); - }, - }; - return createInstance(overrides).then(firestoreClient => { - firestore = firestoreClient; - return firestore._bulkWriter(); - }); - } - it('flush() should not fail', async () => { - const bulkWriter = await instantiateInstance(); - bulkWriter - .create(firestore.doc('collectionId/doc'), {foo: 'bar'}) - .catch(incrementOpCount); - bulkWriter - .set(firestore.doc('collectionId/doc2'), {foo: 'bar'}) - .catch(incrementOpCount); - await bulkWriter.flush(); - verifyOpCount(2); - - return bulkWriter.close(); - }); - - it('close() should not fail', async () => { - const bulkWriter = await instantiateInstance(); - bulkWriter - .create(firestore.doc('collectionId/doc'), {foo: 'bar'}) - .catch(incrementOpCount); - bulkWriter - .set(firestore.doc('collectionId/doc2'), {foo: 'bar'}) - .catch(incrementOpCount); - - return bulkWriter.close().then(() => verifyOpCount(2)); - }); - - it('all individual writes are rejected', async () => { - const bulkWriter = await instantiateInstance(); - bulkWriter - .create(firestore.doc('collectionId/doc'), {foo: 'bar'}) - .catch(err => { - expect(err.message).to.equal('Mock batchWrite failed in test'); - incrementOpCount(); - }); - - bulkWriter - .set(firestore.doc('collectionId/doc2'), {foo: 'bar'}) - .catch(err => { - expect(err.message).to.equal('Mock batchWrite failed in test'); - incrementOpCount(); - }); - - return bulkWriter.close().then(() => verifyOpCount(2)); - }); - }); -}); diff --git a/dev/test/document.ts b/dev/test/document.ts index bc33f9224..19a655815 100644 --- a/dev/test/document.ts +++ b/dev/test/document.ts @@ -913,9 +913,7 @@ describe('set document', () => { requestEquals( request, set({ - document: document('documentId'), transforms: [serverTimestamp('a'), serverTimestamp('b.c')], - mask: updateMask(), }) ); return response(writeResult(1)); @@ -943,7 +941,7 @@ describe('set document', () => { transforms: [serverTimestamp('a'), serverTimestamp('b.c')], }) ); - return response(writeResult(1)); + return response(writeResult(2)); }, }; @@ -1123,7 +1121,7 @@ describe('set document', () => { ], }) ); - return response(writeResult(1)); + return response(writeResult(2)); }, }; @@ -1356,7 +1354,6 @@ describe('create document', () => { requestEquals( request, create({ - document: document('documentId'), transforms: [ serverTimestamp('field'), serverTimestamp('map.field'), @@ -1461,7 +1458,7 @@ describe('update document', () => { mask: updateMask('a', 'foo'), }) ); - return response(writeResult(1)); + return response(writeResult(2)); }, }; @@ -1477,13 +1474,7 @@ describe('update document', () => { it('skips write for single field transform', () => { const overrides: ApiOverride = { commit: request => { - requestEquals( - request, - update({ - document: document('documentId'), - transforms: [serverTimestamp('a')], - }) - ); + requestEquals(request, update({transforms: [serverTimestamp('a')]})); return response(writeResult(1)); }, }; diff --git a/dev/test/field-value.ts b/dev/test/field-value.ts index a49d2387d..4007e142c 100644 --- a/dev/test/field-value.ts +++ b/dev/test/field-value.ts @@ -108,7 +108,7 @@ describe('FieldValue.arrayUnion()', () => { requestEquals(request, expectedRequest); - return response(writeResult(1)); + return response(writeResult(2)); }, }; @@ -172,7 +172,7 @@ describe('FieldValue.increment()', () => { ], }); requestEquals(request, expectedRequest); - return response(writeResult(1)); + return response(writeResult(2)); }, }; @@ -215,7 +215,7 @@ describe('FieldValue.arrayRemove()', () => { }); requestEquals(request, expectedRequest); - return response(writeResult(1)); + return response(writeResult(2)); }, }; @@ -260,7 +260,7 @@ describe('FieldValue.serverTimestamp()', () => { }); requestEquals(request, expectedRequest); - return response(writeResult(1)); + return response(writeResult(2)); }, }; diff --git a/dev/test/rate-limiter.ts b/dev/test/rate-limiter.ts deleted file mode 100644 index e3271303d..000000000 --- a/dev/test/rate-limiter.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {expect} from 'chai'; - -import {RateLimiter} from '../src/rate-limiter'; - -describe('RateLimiter', () => { - let limiter: RateLimiter; - - beforeEach(() => { - limiter = new RateLimiter( - /* initialCapacity= */ 500, - /* multiplier= */ 1.5, - /* multiplierMillis= */ 5 * 60 * 1000, - /* startTime= */ new Date(0).getTime() - ); - }); - - it('accepts and rejects requests based on capacity', () => { - expect(limiter.tryMakeRequest(250, new Date(0).getTime())).to.be.true; - expect(limiter.tryMakeRequest(250, new Date(0).getTime())).to.be.true; - - // Once tokens have been used, further requests should fail. - expect(limiter.tryMakeRequest(1, new Date(0).getTime())).to.be.false; - - // Tokens will only refill up to max capacity. - expect(limiter.tryMakeRequest(501, new Date(1 * 1000).getTime())).to.be - .false; - expect(limiter.tryMakeRequest(500, new Date(1 * 1000).getTime())).to.be - .true; - - // Tokens will refill incrementally based on the number of ms elapsed. - expect(limiter.tryMakeRequest(250, new Date(1 * 1000 + 499).getTime())).to - .be.false; - expect(limiter.tryMakeRequest(249, new Date(1 * 1000 + 500).getTime())).to - .be.true; - - // Scales with multiplier. - expect(limiter.tryMakeRequest(751, new Date((5 * 60 - 1) * 1000).getTime())) - .to.be.false; - expect(limiter.tryMakeRequest(751, new Date(5 * 60 * 1000).getTime())).to.be - .false; - expect(limiter.tryMakeRequest(750, new Date(5 * 60 * 1000).getTime())).to.be - .true; - - // Tokens will never exceed capacity. - expect(limiter.tryMakeRequest(751, new Date((5 * 60 + 3) * 1000).getTime())) - .to.be.false; - - // Rejects requests made before lastRefillTime - expect(() => - limiter.tryMakeRequest(751, new Date((5 * 60 + 2) * 1000).getTime()) - ).to.throw('Request time should not be before the last token refill time.'); - }); - - it('calculates the number of ms needed to place the next request', () => { - // Should return 0 if there are enough tokens for the request to be made. - let timestamp = new Date(0).getTime(); - expect(limiter.getNextRequestDelayMs(500, timestamp)).to.equal(0); - - // Should factor in remaining tokens when calculating the time. - expect(limiter.tryMakeRequest(250, timestamp)); - expect(limiter.getNextRequestDelayMs(500, timestamp)).to.equal(500); - - // Once tokens have been used, should calculate time before next request. - timestamp = new Date(1 * 1000).getTime(); - expect(limiter.tryMakeRequest(500, timestamp)).to.be.true; - expect(limiter.getNextRequestDelayMs(100, timestamp)).to.equal(200); - expect(limiter.getNextRequestDelayMs(250, timestamp)).to.equal(500); - expect(limiter.getNextRequestDelayMs(500, timestamp)).to.equal(1000); - expect(limiter.getNextRequestDelayMs(501, timestamp)).to.equal(-1); - - // Scales with multiplier. - timestamp = new Date(5 * 60 * 1000).getTime(); - expect(limiter.tryMakeRequest(750, timestamp)).to.be.true; - expect(limiter.getNextRequestDelayMs(250, timestamp)).to.equal(334); - expect(limiter.getNextRequestDelayMs(500, timestamp)).to.equal(667); - expect(limiter.getNextRequestDelayMs(750, timestamp)).to.equal(1000); - expect(limiter.getNextRequestDelayMs(751, timestamp)).to.equal(-1); - }); - - it('calculates the maximum number of operations correctly', async () => { - expect(limiter.calculateCapacity(new Date(0).getTime())).to.equal(500); - expect( - limiter.calculateCapacity(new Date(5 * 60 * 1000).getTime()) - ).to.equal(750); - expect( - limiter.calculateCapacity(new Date(10 * 60 * 1000).getTime()) - ).to.equal(1125); - expect( - limiter.calculateCapacity(new Date(15 * 60 * 1000).getTime()) - ).to.equal(1687); - expect( - limiter.calculateCapacity(new Date(90 * 60 * 1000).getTime()) - ).to.equal(738945); - }); -}); diff --git a/dev/test/util/helpers.ts b/dev/test/util/helpers.ts index f78ccaa9b..655b73d87 100644 --- a/dev/test/util/helpers.ts +++ b/dev/test/util/helpers.ts @@ -89,23 +89,27 @@ export function verifyInstance(firestore: Firestore): Promise { } function write( - document: api.IDocument, + document: api.IDocument | null, mask: api.IDocumentMask | null, transforms: api.DocumentTransform.IFieldTransform[] | null, precondition: api.IPrecondition | null ): api.ICommitRequest { const writes: api.IWrite[] = []; - const update = Object.assign({}, document); - delete update.updateTime; - delete update.createTime; - writes.push({update}); - if (mask) { - writes[0].updateMask = mask; + if (document) { + const update = Object.assign({}, document); + delete update.updateTime; + delete update.createTime; + writes.push({update}); + if (mask) { + writes[0].updateMask = mask; + } } if (transforms) { - writes[0].updateTransforms = transforms; + writes.push({ + transform: {document: DOCUMENT_NAME, fieldTransforms: transforms}, + }); } if (precondition) { @@ -120,37 +124,47 @@ export function updateMask(...fieldPaths: string[]): api.IDocumentMask { } export function set(opts: { - document: api.IDocument; + document?: api.IDocument; transforms?: api.DocumentTransform.IFieldTransform[]; mask?: api.IDocumentMask; }): api.ICommitRequest { return write( - opts.document, + opts.document || null, opts.mask || null, opts.transforms || null, - /* precondition= */ null + null ); } export function update(opts: { - document: api.IDocument; + document?: api.IDocument; transforms?: api.DocumentTransform.IFieldTransform[]; mask?: api.IDocumentMask; precondition?: api.IPrecondition; }): api.ICommitRequest { const precondition = opts.precondition || {exists: true}; const mask = opts.mask || updateMask(); - return write(opts.document, mask, opts.transforms || null, precondition); + return write( + opts.document || null, + mask, + opts.transforms || null, + precondition + ); } export function create(opts: { - document: api.IDocument; + document?: api.IDocument; transforms?: api.DocumentTransform.IFieldTransform[]; mask?: api.IDocumentMask; }): api.ICommitRequest { - return write(opts.document, /* updateMask= */ null, opts.transforms || null, { - exists: false, - }); + return write( + opts.document || null, + /* updateMask */ null, + opts.transforms || null, + { + exists: false, + } + ); } function value(value: string | api.IValue): api.IValue { diff --git a/dev/test/write-batch.ts b/dev/test/write-batch.ts index 76b348afb..692d40abb 100644 --- a/dev/test/write-batch.ts +++ b/dev/test/write-batch.ts @@ -14,7 +14,6 @@ import {expect} from 'chai'; -import {Status} from 'google-gax'; import { FieldValue, Firestore, @@ -23,7 +22,6 @@ import { WriteBatch, WriteResult, } from '../src'; -import {BatchWriteResult} from '../src/write-batch'; import { ApiOverride, createInstance, @@ -66,8 +64,8 @@ describe('set() method', () => { ); }); - it('accepts document data', () => { - writeBatch.set(firestore.doc('sub/doc'), {foo: 'bar'}); + it('accepts preconditions', () => { + writeBatch.set(firestore.doc('sub/doc'), {exists: false}); }); it('works with null objects', () => { @@ -199,12 +197,17 @@ describe('batch support', () => { fields: {}, name: documentName, }, - updateTransforms: [ - { - fieldPath: 'foo', - setToServerValue: REQUEST_TIME, - }, - ], + }, + { + transform: { + document: documentName, + fieldTransforms: [ + { + fieldPath: 'foo', + setToServerValue: REQUEST_TIME, + }, + ], + }, }, { currentDocument: { @@ -242,6 +245,14 @@ describe('batch support', () => { seconds: 0, }, writeResults: [ + // This write result conforms to the Write + + // DocumentTransform and won't be returned in the response. + { + updateTime: { + nanos: 1337, + seconds: 1337, + }, + }, { updateTime: { nanos: 0, @@ -464,88 +475,3 @@ describe('batch support', () => { }); }); }); - -describe('bulkCommit support', () => { - const documentName = `projects/${PROJECT_ID}/databases/(default)/documents/col/doc`; - - let firestore: Firestore; - let writeBatch: WriteBatch; - - beforeEach(() => { - const overrides: ApiOverride = { - batchWrite: request => { - expect(request).to.deep.eq({ - database: `projects/${PROJECT_ID}/databases/(default)`, - writes: [ - { - update: { - fields: {}, - name: documentName, - }, - updateTransforms: [ - { - fieldPath: 'foo', - setToServerValue: REQUEST_TIME, - }, - ], - }, - { - currentDocument: { - exists: true, - }, - update: { - fields: { - foo: { - stringValue: 'bar', - }, - }, - name: documentName, - }, - updateMask: { - fieldPaths: ['foo'], - }, - }, - ], - }); - return response({ - writeResults: [ - { - updateTime: { - nanos: 0, - seconds: 0, - }, - }, - { - updateTime: null, - }, - ], - status: [{code: 0}, {code: 14}], - }); - }, - }; - return createInstance(overrides).then(firestoreClient => { - firestore = firestoreClient; - writeBatch = firestore.batch(); - }); - }); - - afterEach(() => verifyInstance(firestore)); - - function verifyResponse(writeResults: BatchWriteResult[]) { - expect(writeResults[0].writeTime!.isEqual(new Timestamp(0, 0))).to.be.true; - expect(writeResults[1].writeTime).to.be.null; - expect(writeResults[0].status.code).to.equal(Status.OK); - expect(writeResults[1].status.code).to.equal(Status.UNAVAILABLE); - } - - it('bulkCommit', () => { - const documentName = firestore.doc('col/doc'); - - writeBatch.set(documentName, {foo: FieldValue.serverTimestamp()}); - writeBatch.update(documentName, {foo: 'bar'}); - - return writeBatch.bulkCommit().then(resp => { - verifyResponse(resp); - }); - }); -}); diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 6f2d2734c..165da504d 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -301,6 +301,7 @@ declare namespace FirebaseFirestore { export class Transaction { private constructor(); + /** * Retrieves a query result. Holds a pessimistic lock on all returned * documents.