Skip to content

Commit 468f8d2

Browse files
bbrksCopilot
andauthored
CBG-5117: Support request-provided oldDoc values in Sync Function Dry-Run (#8019)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9a22b95 commit 468f8d2

File tree

4 files changed

+87
-35
lines changed

4 files changed

+87
-35
lines changed

db/crud.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,10 +1699,10 @@ func (db *DatabaseCollectionWithUser) PutExistingRevWithBody(ctx context.Context
16991699

17001700
}
17011701

1702-
// SyncFnDryrun Runs the given document body through a sync function and returns expiry, channels doc was placed in,
1702+
// SyncFnDryRun Runs the given document body through a sync function and returns expiry, channels doc was placed in,
17031703
// access map for users, roles, handler errors and sync fn exceptions.
17041704
// If syncFn is provided, it will be used instead of the one configured on the database.
1705-
func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, newDoc, oldDoc *Document, userMeta, syncOptions map[string]any, syncFn string, errorLogFunc, infoLogFunc func(string)) (*channels.ChannelMapperOutput, error) {
1705+
func (db *DatabaseCollectionWithUser) SyncFnDryRun(ctx context.Context, newDoc, oldDoc *Document, userMeta, syncOptions map[string]any, syncFn string, errorLogFunc, infoLogFunc func(string)) (*channels.ChannelMapperOutput, error) {
17061706
mutableBody, metaMap, _, err := db.prepareSyncFn(oldDoc, newDoc)
17071707
if err != nil {
17081708
base.InfofCtx(ctx, base.KeyDiagnostic, "Failed to prepare to run sync function: %v", err)
@@ -1737,7 +1737,11 @@ func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, newDoc,
17371737
return nil, fmt.Errorf("failed to create sync runner: %v", err)
17381738
}
17391739

1740-
jsOutput, err := syncRunner.Call(ctx, mutableBody, sgbucket.JSONString(oldDoc._rawBody), metaMap, syncOptions)
1740+
oldDocBodyBytes, err := oldDoc.BodyBytes(ctx)
1741+
if err != nil {
1742+
return nil, err
1743+
}
1744+
jsOutput, err := syncRunner.Call(ctx, mutableBody, sgbucket.JSONString(oldDocBodyBytes), metaMap, syncOptions)
17411745
if err != nil {
17421746
return nil, &base.SyncFnDryRunError{Err: err}
17431747
}

docs/api/paths/diagnostic/keyspace-sync.yaml

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ post:
1515
If no custom sync function is provided in the request, the user-defined sync function for the collection is used (or the default).
1616
1717
The document being run through the sync function can be provided in one of the following ways:
18-
| `doc` property | `doc_id` property | Behaviour |
19-
| ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20-
| Set | Unset | The document body passed will be the `doc` value in the sync function, and `oldDoc` will be empty. |
21-
| Unset | Set | The document of the given id will be fetched from the bucket/collection and will be passed in as the `doc` value in the sync function. If the document is not found, an error will be returned |
22-
| Set | Set | The document body passed will be the `doc` value in the sync function, and the `doc_id` will be fetched from the bucket/collection and will be the `oldDoc` value. If `doc_id` doesn't exist, then `oldDoc` will be empty |
23-
| Unset | Unset | Will throw an error (at least one of `doc` or `doc_id` is required) |
18+
| `doc` property | `oldDoc` property | `doc_id` property | Behaviour |
19+
| ----- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20+
| Set | Unset | Unset | The document body passed will be the `doc` value in the sync function, and `oldDoc` will be empty. |
21+
| Set | Set | Unset | The document body passed will be the `doc` value in the sync function, and the `oldDoc` value will be set to the inline `oldDoc` request body. |
22+
| Unset | Unset | Set | The document of the given id will be fetched from the bucket/collection and will be passed in as the `doc` value in the sync function. If the document is not found, an error will be returned |
23+
| Set | Unset | Set | The document body passed will be the `doc` value in the sync function, and the `doc_id` will be fetched from the bucket/collection and will be the `oldDoc` value. If `doc_id` doesn't exist, then `oldDoc` will be empty |
24+
| Unset | - | Unset | Will throw an error (at least one of `doc` or `doc_id` is required) |
25+
| - | Set | Set | Will throw an error (`doc_id` and inline `oldDoc` cannot both be specified) |
2426
2527
* Sync Gateway Application Read Only
2628
requestBody:
@@ -45,11 +47,19 @@ post:
4547
channel(doc.channels);
4648
}
4749
doc:
48-
description: A new document body to run the dry-run operation against.
50+
description: A document body to run the sync function dry-run operation against.
51+
type: object
52+
additionalProperties: true
53+
example:
54+
foo: buzz
55+
doc_num_updates: 2
56+
oldDoc:
57+
description: A document body to use as `oldDoc` during the dry-run. Cannot be used with `doc_id`.
4958
type: object
5059
additionalProperties: true
5160
example:
5261
foo: bar
62+
doc_num_updates: 1
5363
meta:
5464
description: Optional metadata used during evaluation.
5565
type: object

rest/diagnostic_doc_api.go

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ type SyncFnDryRunPayload struct {
5353
DocID string `json:"doc_id"`
5454
Function string `json:"sync_function"`
5555
Doc db.Body `json:"doc,omitempty"`
56+
OldDoc db.Body `json:"oldDoc,omitempty"`
5657
Meta SyncFnDryRunMetaMap `json:"meta,omitempty"`
5758
UserCtx *SyncDryRunUserCtx `json:"userCtx,omitempty"`
5859
}
5960

60-
const SYNC_FN_DIAGNOSTIC_DOCID = "diagnostic_doc"
61+
const defaultSyncDryRunDocID = "sync_dryrun"
6162

6263
type ImportFilterDryRunPayload struct {
6364
DocID string `json:"doc_id"`
@@ -95,10 +96,12 @@ func (h *handler) handleGetDocChannels() error {
9596
}
9697

9798
// HTTP handler for running a document through the sync function and returning the results
98-
// body only provided, the sync function will run with no oldDoc provided
99-
// body and doc ID provided, the sync function will run using the current revision in the bucket as oldDoc
100-
// docid only provided, the sync function will run using the current revision in the bucket as doc
101-
// If docid is specified and the document does not exist in the bucket, it will return error
99+
//
100+
// The sync function can be dry-run with the following combinations of doc properties:
101+
// - If 'doc' only provided, the sync function will run with the provided doc (and no oldDoc)
102+
// - If 'doc' and 'oldDoc' provided, the sync function will run using the provided doc and oldDoc
103+
// - If 'doc_id' only provided, the sync function will run using the current revision in the bucket as doc
104+
// - If 'doc' and 'doc_id' provided, the sync function will run using the provided doc and the current revision in the bucket as oldDoc
102105
func (h *handler) handleSyncFnDryRun() error {
103106

104107
var syncDryRunPayload SyncFnDryRunPayload
@@ -109,15 +112,18 @@ func (h *handler) handleSyncFnDryRun() error {
109112

110113
bucketDocID := syncDryRunPayload.DocID
111114
if syncDryRunPayload.Doc == nil && bucketDocID == "" {
112-
return base.HTTPErrorf(http.StatusBadRequest, "no doc_id or document provided")
115+
return base.HTTPErrorf(http.StatusBadRequest, "must provide either doc_id or doc")
116+
}
117+
if syncDryRunPayload.OldDoc != nil && bucketDocID != "" {
118+
return base.HTTPErrorf(http.StatusBadRequest, "cannot specify doc_id with a provided oldDoc")
113119
}
114120

115121
var userXattrs map[string]any
116122
// checking user defined metadata
117123
if syncDryRunPayload.Meta.Xattrs != nil {
118124
xattrs := syncDryRunPayload.Meta.Xattrs
119125
if len(xattrs) > 1 {
120-
return base.HTTPErrorf(http.StatusBadRequest, "Only one xattr key can be specified in meta")
126+
return base.HTTPErrorf(http.StatusBadRequest, "only one xattr key can be specified in meta")
121127
}
122128
userXattrKey := h.collection.UserXattrKey()
123129
if userXattrKey == "" {
@@ -154,26 +160,29 @@ func (h *handler) handleSyncFnDryRun() error {
154160
userCtx["roles"] = rolesMap
155161
}
156162

157-
var docid string
158-
159-
id, _ := syncDryRunPayload.Doc[db.BodyId].(string)
160-
docid = cmp.Or(bucketDocID, id, SYNC_FN_DIAGNOSTIC_DOCID)
163+
inlineDocID, _ := syncDryRunPayload.Doc[db.BodyId].(string)
164+
docID := cmp.Or(bucketDocID, inlineDocID, defaultSyncDryRunDocID)
161165

162-
oldDoc := &db.Document{ID: docid}
163-
oldDoc.UpdateBody(syncDryRunPayload.Doc)
166+
oldDoc := &db.Document{ID: docID}
164167
// Read the document from the bucket
165168
if bucketDocID != "" {
166-
if docInbucket, err := h.collection.GetDocument(h.ctx(), bucketDocID, db.DocUnmarshalAll); err == nil {
167-
oldDoc = docInbucket
168-
if len(syncDryRunPayload.Doc) == 0 {
169-
syncDryRunPayload.Doc = oldDoc.Body(h.ctx())
170-
oldDoc.UpdateBody(nil)
171-
}
169+
bucketDoc, err := h.collection.GetDocument(h.ctx(), bucketDocID, db.DocUnmarshalAll)
170+
if err != nil {
171+
return err
172+
}
173+
if len(syncDryRunPayload.Doc) == 0 {
174+
// use bucket doc as current doc value
175+
syncDryRunPayload.Doc = bucketDoc.Body(h.ctx())
176+
// set oldDoc for any xattrs that may be present for use in `meta`, but nil the body
177+
oldDoc = bucketDoc
178+
oldDoc.UpdateBody(nil)
172179
} else {
173-
return base.HTTPErrorf(http.StatusNotFound, "Error reading document: %v", err)
180+
// use bucket doc as oldDoc value
181+
oldDoc = bucketDoc
174182
}
175-
} else {
176-
oldDoc.UpdateBody(nil)
183+
} else if syncDryRunPayload.OldDoc != nil {
184+
// use inline oldDoc value
185+
oldDoc.UpdateBody(syncDryRunPayload.OldDoc)
177186
}
178187

179188
delete(syncDryRunPayload.Doc, db.BodyId)
@@ -188,7 +197,7 @@ func (h *handler) handleSyncFnDryRun() error {
188197

189198
// Create newDoc which will be used to pass around Body
190199
newDoc := &db.Document{
191-
ID: docid,
200+
ID: docID,
192201
}
193202
// Pull attachments
194203
newDoc.SetAttachments(db.GetBodyAttachments(syncDryRunPayload.Doc))
@@ -227,7 +236,7 @@ func (h *handler) handleSyncFnDryRun() error {
227236
logInfo = append(logInfo, s)
228237
}
229238

230-
output, err := h.collection.SyncFnDryrun(h.ctx(), newDoc, oldDoc, userXattrs, userCtx, syncDryRunPayload.Function, errorLogFn, infoLogFn)
239+
output, err := h.collection.SyncFnDryRun(h.ctx(), newDoc, oldDoc, userXattrs, userCtx, syncDryRunPayload.Function, errorLogFn, infoLogFn)
231240
if err != nil {
232241
var syncFnDryRunErr *base.SyncFnDryRunError
233242
if !errors.As(err, &syncFnDryRunErr) {

rest/diagnostic_doc_api_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,31 @@ func TestSyncFuncDryRun(t *testing.T) {
11161116
},
11171117
expectedStatus: http.StatusOK,
11181118
},
1119+
{
1120+
name: "request sync function and doc with inline oldDoc",
1121+
request: SyncFnDryRunPayload{
1122+
Function: `function(doc, oldDoc) {
1123+
if (doc) {
1124+
channel(doc.newDoc);
1125+
};
1126+
if (oldDoc) {
1127+
channel(oldDoc.oldDoc);
1128+
};
1129+
}`,
1130+
Doc: db.Body{"newDoc": "newdoc_channel"},
1131+
OldDoc: db.Body{"oldDoc": "olddoc_channel"},
1132+
},
1133+
expectedOutput: SyncFnDryRun{
1134+
Channels: base.SetOf("newdoc_channel", "olddoc_channel"),
1135+
Access: channels.AccessMap{},
1136+
Roles: channels.AccessMap{},
1137+
Logging: DryRunLogging{
1138+
Errors: []string{},
1139+
Info: []string{},
1140+
},
1141+
},
1142+
expectedStatus: http.StatusOK,
1143+
},
11191144
{
11201145
name: "sync func exception",
11211146
request: SyncFnDryRunPayload{
@@ -1411,7 +1436,7 @@ func TestSyncFuncDryRun(t *testing.T) {
14111436
Roles: channels.AccessMap{},
14121437
Logging: DryRunLogging{
14131438
Errors: []string{},
1414-
Info: []string{fmt.Sprintf("{\"_id\":\"%s\",\"_rev\":\"1-cd809becc169215072fd567eebd8b8de\",\"foo\":\"bar\"}", SYNC_FN_DIAGNOSTIC_DOCID)},
1439+
Info: []string{fmt.Sprintf("{\"_id\":\"%s\",\"_rev\":\"1-cd809becc169215072fd567eebd8b8de\",\"foo\":\"bar\"}", defaultSyncDryRunDocID)},
14151440
},
14161441
},
14171442
expectedStatus: http.StatusOK,
@@ -1635,6 +1660,10 @@ func TestSyncFuncDryRunErrors(t *testing.T) {
16351660

16361661
// doc ID not found
16371662
RequireStatus(t, rt.SendDiagnosticRequest(http.MethodPost, "/{{.keyspace}}/_sync", `{"doc_id": "missing"}`), http.StatusNotFound)
1663+
// only oldDoc provided
1664+
RequireStatus(t, rt.SendDiagnosticRequest(http.MethodPost, "/{{.keyspace}}/_sync", `{"oldDoc": {"foo":"old"}}`), http.StatusBadRequest)
1665+
// doc ID and inline oldDoc provided
1666+
RequireStatus(t, rt.SendDiagnosticRequest(http.MethodPost, "/{{.keyspace}}/_sync", `{"doc_id": "somedoc", "oldDoc": {"foo":"old"}}`), http.StatusBadRequest)
16381667
// no doc ID or inline body provided
16391668
RequireStatus(t, rt.SendDiagnosticRequest(http.MethodPost, "/{{.keyspace}}/_sync", `{}`), http.StatusBadRequest)
16401669
// invalid request json

0 commit comments

Comments
 (0)