@@ -4,11 +4,19 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "os"
78 "strings"
89
910 "google.golang.org/api/docs/v1"
1011)
1112
13+ // docsBatchUpdateRequestCap is the Docs API hard limit on the number of
14+ // requests a single documents.batchUpdate may carry. When the consolidated
15+ // body + table + formatting request list exceeds it we chunk into multiple
16+ // sequential batchUpdate calls, preserving the request order so cell-index
17+ // arithmetic stays consistent. See #699.
18+ const docsBatchUpdateRequestCap = 500
19+
1220const (
1321 docsContentFormatPlain = "plain"
1422 docsContentFormatMarkdown = "markdown"
@@ -127,21 +135,13 @@ func replaceDocsMarkdownRange(ctx context.Context, svc *docs.Service, doc *docs.
127135 }
128136 formattingRequests , textToInsert , tables := MarkdownToDocsRequests (elements , baseIndex , tabID )
129137
130- for _ , req := range formattingRequests {
131- if req .UpdateTextStyle != nil && req .UpdateTextStyle .Range != nil {
132- req .UpdateTextStyle .Range .TabId = tabID
133- }
134- if req .UpdateParagraphStyle != nil && req .UpdateParagraphStyle .Range != nil {
135- req .UpdateParagraphStyle .Range .TabId = tabID
136- }
137- if req .CreateParagraphBullets != nil && req .CreateParagraphBullets .Range != nil {
138- req .CreateParagraphBullets .Range .TabId = tabID
139- }
140- if req .DeleteParagraphBullets != nil && req .DeleteParagraphBullets .Range != nil {
141- req .DeleteParagraphBullets .Range .TabId = tabID
142- }
143- }
138+ applyTabIDToFormattingRequests (formattingRequests , tabID )
144139
140+ // Structural DeleteContentRange + body InsertText + per-element formatting
141+ // go in one batchUpdate. Tables are inserted afterwards via InsertNativeTable
142+ // (which does its own InsertTable + Get + batched cell-content per table —
143+ // see #699 follow-up: cross-table prediction was unreliable, server-readback
144+ // per table is correct).
145145 requests := make ([]* docs.Request , 0 , 2 + len (formattingRequests ))
146146 requests = append (requests , & docs.Request {
147147 DeleteContentRange : & docs.DeleteContentRangeRequest {
@@ -158,14 +158,10 @@ func replaceDocsMarkdownRange(ctx context.Context, svc *docs.Service, doc *docs.
158158 requests = append (requests , formattingRequests ... )
159159 }
160160
161- _ , err = svc .Documents .BatchUpdate (doc .DocumentId , & docs.BatchUpdateDocumentRequest {
162- WriteControl : & docs.WriteControl {RequiredRevisionId : doc .RevisionId },
163- Requests : requests ,
164- }).Context (ctx ).Do ()
161+ requestCount , err = submitBatchedDocsRequests (ctx , svc , doc .DocumentId , requests , & docs.WriteControl {RequiredRevisionId : doc .RevisionId })
165162 if err != nil {
166163 return 0 , 0 , fmt .Errorf ("replace (markdown): %w" , err )
167164 }
168- requestCount = len (requests )
169165
170166 if len (tables ) > 0 {
171167 tableInserter := NewTableInserter (svc , doc .DocumentId )
@@ -174,7 +170,7 @@ func replaceDocsMarkdownRange(ctx context.Context, svc *docs.Service, doc *docs.
174170 tableIndex := table .StartIndex + tableOffset
175171 tableEnd , tableErr := tableInserter .InsertNativeTable (ctx , tableIndex , table .Cells , tabID )
176172 if tableErr != nil {
177- return requestCount , len (prefix ) + len ( textToInsert ), fmt .Errorf ("insert native table: %w" , tableErr )
173+ return requestCount , len (textToInsert ), fmt .Errorf ("insert native table: %w" , tableErr )
178174 }
179175 tableOffset = nextTableInsertOffset (tableOffset , tableIndex , tableEnd )
180176 }
@@ -209,21 +205,11 @@ func insertDocsMarkdownAt(ctx context.Context, svc *docs.Service, docID string,
209205 return 0 , 0 , nil
210206 }
211207
212- for _ , req := range formattingRequests {
213- if req .UpdateTextStyle != nil && req .UpdateTextStyle .Range != nil {
214- req .UpdateTextStyle .Range .TabId = tabID
215- }
216- if req .UpdateParagraphStyle != nil && req .UpdateParagraphStyle .Range != nil {
217- req .UpdateParagraphStyle .Range .TabId = tabID
218- }
219- if req .CreateParagraphBullets != nil && req .CreateParagraphBullets .Range != nil {
220- req .CreateParagraphBullets .Range .TabId = tabID
221- }
222- if req .DeleteParagraphBullets != nil && req .DeleteParagraphBullets .Range != nil {
223- req .DeleteParagraphBullets .Range .TabId = tabID
224- }
225- }
208+ applyTabIDToFormattingRequests (formattingRequests , tabID )
226209
210+ // Body InsertText + per-element formatting in one batchUpdate. Tables
211+ // follow via InsertNativeTable (one InsertTable + one cell batch per
212+ // table — see #699 follow-up).
227213 requests := make ([]* docs.Request , 0 , 1 + len (formattingRequests ))
228214 requests = append (requests , & docs.Request {
229215 InsertText : & docs.InsertTextRequest {
@@ -233,9 +219,7 @@ func insertDocsMarkdownAt(ctx context.Context, svc *docs.Service, docID string,
233219 })
234220 requests = append (requests , formattingRequests ... )
235221
236- _ , err = svc .Documents .BatchUpdate (docID , & docs.BatchUpdateDocumentRequest {
237- Requests : requests ,
238- }).Context (ctx ).Do ()
222+ requestCount , err = submitBatchedDocsRequests (ctx , svc , docID , requests , nil )
239223 if err != nil {
240224 return 0 , 0 , fmt .Errorf ("append (markdown): %w" , err )
241225 }
@@ -247,7 +231,7 @@ func insertDocsMarkdownAt(ctx context.Context, svc *docs.Service, docID string,
247231 tableIndex := table .StartIndex + tableOffset
248232 tableEnd , tableErr := tableInserter .InsertNativeTable (ctx , tableIndex , table .Cells , tabID )
249233 if tableErr != nil {
250- return len ( requests ) , len (textToInsert ), fmt .Errorf ("insert native table: %w" , tableErr )
234+ return requestCount , len (textToInsert ), fmt .Errorf ("insert native table: %w" , tableErr )
251235 }
252236 tableOffset = nextTableInsertOffset (tableOffset , tableIndex , tableEnd )
253237 }
@@ -257,11 +241,86 @@ func insertDocsMarkdownAt(ctx context.Context, svc *docs.Service, docID string,
257241 imgErr := insertImagesIntoDocs (ctx , svc , docID , images , tabID )
258242 cleanupDocsImagePlaceholders (ctx , svc , docID , images , tabID )
259243 if imgErr != nil {
260- return len (requests ), len (textToInsert ), fmt .Errorf ("insert images: %w" , imgErr )
244+ return requestCount , len (prefix ) + len (textToInsert ), fmt .Errorf ("insert images: %w" , imgErr )
261245 }
262246 }
263247
264- return len (requests ), len (prefix ) + len (textToInsert ), nil
248+ return requestCount , len (prefix ) + len (textToInsert ), nil
249+ }
250+
251+ // applyTabIDToFormattingRequests propagates tabID to every request whose
252+ // range needs to be tab-scoped. Centralised so both the append and replace
253+ // markdown paths stay in sync — previously each duplicated the same eight
254+ // nil-guarded assignments inline.
255+ func applyTabIDToFormattingRequests (requests []* docs.Request , tabID string ) {
256+ if tabID == "" {
257+ return
258+ }
259+ for _ , req := range requests {
260+ if req == nil {
261+ continue
262+ }
263+ if req .UpdateTextStyle != nil && req .UpdateTextStyle .Range != nil {
264+ req .UpdateTextStyle .Range .TabId = tabID
265+ }
266+ if req .UpdateParagraphStyle != nil && req .UpdateParagraphStyle .Range != nil {
267+ req .UpdateParagraphStyle .Range .TabId = tabID
268+ }
269+ if req .CreateParagraphBullets != nil && req .CreateParagraphBullets .Range != nil {
270+ req .CreateParagraphBullets .Range .TabId = tabID
271+ }
272+ if req .DeleteParagraphBullets != nil && req .DeleteParagraphBullets .Range != nil {
273+ req .DeleteParagraphBullets .Range .TabId = tabID
274+ }
275+ }
276+ }
277+
278+ // submitBatchedDocsRequests sends the supplied request list as one or more
279+ // documents.batchUpdate calls, splitting at docsBatchUpdateRequestCap-sized
280+ // chunks when the consolidated request count exceeds the Docs API per-batch
281+ // hard limit. Each chunk preserves the source order so cell-index
282+ // arithmetic remains consistent across the split. Returns the total number
283+ // of requests submitted (matches len(requests) on success); chunk events are
284+ // announced on stderr so callers can correlate wire traffic with logs.
285+ func submitBatchedDocsRequests (ctx context.Context , svc * docs.Service , docID string , requests []* docs.Request , writeControl * docs.WriteControl ) (int , error ) {
286+ if len (requests ) == 0 {
287+ return 0 , nil
288+ }
289+ if len (requests ) <= docsBatchUpdateRequestCap {
290+ _ , err := svc .Documents .BatchUpdate (docID , & docs.BatchUpdateDocumentRequest {
291+ WriteControl : writeControl ,
292+ Requests : requests ,
293+ }).Context (ctx ).Do ()
294+ if err != nil {
295+ return 0 , err
296+ }
297+ return len (requests ), nil
298+ }
299+
300+ totalChunks := (len (requests ) + docsBatchUpdateRequestCap - 1 ) / docsBatchUpdateRequestCap
301+ for i := 0 ; i < len (requests ); i += docsBatchUpdateRequestCap {
302+ end := i + docsBatchUpdateRequestCap
303+ if end > len (requests ) {
304+ end = len (requests )
305+ }
306+ chunkIdx := i / docsBatchUpdateRequestCap + 1
307+ fmt .Fprintf (os .Stderr , "gog: docs batchUpdate split %d/%d (%d requests; Docs API per-call cap is %d)\n " ,
308+ chunkIdx , totalChunks , end - i , docsBatchUpdateRequestCap )
309+ // WriteControl is only meaningful on the first chunk — subsequent
310+ // chunks operate on whatever revision the prior chunk produced.
311+ var wc * docs.WriteControl
312+ if i == 0 {
313+ wc = writeControl
314+ }
315+ _ , err := svc .Documents .BatchUpdate (docID , & docs.BatchUpdateDocumentRequest {
316+ WriteControl : wc ,
317+ Requests : requests [i :end ],
318+ }).Context (ctx ).Do ()
319+ if err != nil {
320+ return i , err
321+ }
322+ }
323+ return len (requests ), nil
265324}
266325
267326func markdownAppendNeedsParagraphBoundary (elements []MarkdownElement ) bool {
0 commit comments