@@ -12,6 +12,7 @@ package base
12
12
13
13
import (
14
14
"context"
15
+ "errors"
15
16
"fmt"
16
17
"slices"
17
18
"strings"
@@ -51,6 +52,7 @@ type N1QLStore interface {
51
52
GetName () string
52
53
BuildDeferredIndexes (ctx context.Context , indexSet []string ) error
53
54
CreateIndex (ctx context.Context , indexName string , expression string , filterExpression string , options * N1qlIndexOptions ) error
55
+ CreateIndexIfNotExists (ctx context.Context , indexName string , expression string , filterExpression string , options * N1qlIndexOptions ) error
54
56
CreatePrimaryIndex (ctx context.Context , indexName string , options * N1qlIndexOptions ) error
55
57
DropIndex (ctx context.Context , indexName string ) error
56
58
ExplainQuery (ctx context.Context , statement string , params map [string ]interface {}) (plan map [string ]interface {}, err error )
@@ -75,6 +77,8 @@ type N1QLStore interface {
75
77
76
78
// waitUntilQueryServiceReady waits until the query service is ready to accept requests
77
79
waitUntilQueryServiceReady (timeout time.Duration ) error
80
+
81
+ sgbucket.BucketStoreFeatureIsSupported
78
82
}
79
83
80
84
func ExplainQuery (ctx context.Context , store N1QLStore , statement string , params map [string ]interface {}) (plan map [string ]interface {}, err error ) {
@@ -119,7 +123,7 @@ func (im *indexManager) GetAllIndexes() ([]gocb.QueryIndex, error) {
119
123
return im .cluster .GetAllIndexes (im .bucketName , opts )
120
124
}
121
125
122
- // CreateIndex issues a CREATE INDEX query in the current bucket , using the form:
126
+ // CreateIndex issues a CREATE INDEX query in the N1QLStore keyspace , using the form:
123
127
//
124
128
// CREATE INDEX indexName ON bucket.Name(expression) WHERE filterExpression WITH options
125
129
//
@@ -128,31 +132,60 @@ func (im *indexManager) GetAllIndexes() ([]gocb.QueryIndex, error) {
128
132
// CreateIndex("myIndex", "field1, field2, nested.field", "field1 > 0", N1qlIndexOptions{numReplica:1})
129
133
// CREATE INDEX myIndex on myBucket(field1, field2, nested.field) WHERE field1 > 0 WITH {"numReplica":1}
130
134
func CreateIndex (ctx context.Context , store N1QLStore , indexName string , expression string , filterExpression string , options * N1qlIndexOptions ) error {
131
- createStatement := fmt .Sprintf ("CREATE INDEX `%s` ON %s(%s)" , indexName , store .EscapedKeyspace (), expression )
135
+ return createIndex (ctx , store , indexName , expression , filterExpression , false , options )
136
+ }
137
+
138
+ // CreateIndexIfNotExists issues a CREATE INDEX query in the N1QLStore keyspace, using the form:
139
+ //
140
+ // CREATE INDEX indexName ON bucket.Name(expression) IF NOT EXISTS WHERE filterExpression WITH options
141
+ //
142
+ // Sample usage with resulting statement:
143
+ //
144
+ // CreateIndex("myIndex", "field1, field2, nested.field", "field1 > 0", N1qlIndexOptions{numReplica:1})
145
+ // CREATE INDEX myIndex on myBucket(field1, field2, nested.field) WHERE field1 > 0 WITH {"numReplica":1}
146
+ func CreateIndexIfNotExists (ctx context.Context , store N1QLStore , indexName string , expression string , filterExpression string , options * N1qlIndexOptions ) error {
147
+ return createIndex (ctx , store , indexName , expression , filterExpression , true , options )
148
+ }
149
+
150
+ // createIndex is a common function for CreateIndex and CreateIndexIfNotExists
151
+ func createIndex (ctx context.Context , store N1QLStore , indexName string , expression string , filterExpression string , ifNotExists bool , options * N1qlIndexOptions ) error {
152
+ var ifNotExistsStr string
153
+ // Server 7.1+ - we can still safely _not_ use this when it's not available, because we have equivalent error handling inside this function to swallow `ErrAlreadyExists`.
154
+ // Would still prefer to use it when we can, to guard us against future error string changes, which is why we do both conditionally.
155
+ if ifNotExists && store .IsSupported (sgbucket .BucketStoreFeatureN1qlIfNotExistsDDL ) {
156
+ ifNotExistsStr = " IF NOT EXISTS"
157
+ }
132
158
133
159
// Add filter expression, when present
160
+ var filterExpressionStr string
134
161
if filterExpression != "" {
135
- createStatement = fmt . Sprintf ( "%s WHERE %s" , createStatement , filterExpression )
162
+ filterExpressionStr = " WHERE " + filterExpression
136
163
}
137
164
138
- // Replace any KeyspaceQueryToken references in the index expression
139
- createStatement = strings .Replace (createStatement , KeyspaceQueryToken , store .EscapedKeyspace (), - 1 )
165
+ createStatement := fmt .Sprintf ("CREATE INDEX `%s`%s ON %s(%s)%s" , indexName , ifNotExistsStr , store .EscapedKeyspace (), expression , filterExpressionStr )
140
166
141
- createErr := createIndex (ctx , store , indexName , createStatement , options )
142
- if createErr != nil {
143
- if strings .Contains (createErr .Error (), "already exists" ) || strings .Contains (createErr .Error (), "duplicate index name" ) {
144
- return ErrAlreadyExists
167
+ // Replace any KeyspaceQueryToken references in the index expression
168
+ createStatement = strings .ReplaceAll (createStatement , KeyspaceQueryToken , store .EscapedKeyspace ())
169
+ createErr := createIndexFromStatement (ctx , store , indexName , createStatement , options )
170
+ if IsIndexAlreadyExistsError (createErr ) || IsCreateDuplicateIndexError (createErr ) {
171
+ // Pre-7.1 compatibility: Swallow this error like Server does when specifying `IF NOT EXISTS`
172
+ if ifNotExists {
173
+ return nil
145
174
}
175
+ return ErrAlreadyExists
146
176
}
147
177
return createErr
148
178
}
149
179
150
180
func CreatePrimaryIndex (ctx context.Context , store N1QLStore , indexName string , options * N1qlIndexOptions ) error {
151
181
createStatement := fmt .Sprintf ("CREATE PRIMARY INDEX `%s` ON %s" , indexName , store .EscapedKeyspace ())
152
- return createIndex (ctx , store , indexName , createStatement , options )
182
+ return createIndexFromStatement (ctx , store , indexName , createStatement , options )
153
183
}
154
184
155
- func createIndex (ctx context.Context , store N1QLStore , indexName string , createStatement string , options * N1qlIndexOptions ) error {
185
+ // ErrIndexBackgroundRetry is returned when an index creation operation returned an error but just needs to wait for a server-side readiness or retry.
186
+ var ErrIndexBackgroundRetry = errors .New ("Indexer error - waiting for server background retry" )
187
+
188
+ func createIndexFromStatement (ctx context.Context , store N1QLStore , indexName string , createStatement string , options * N1qlIndexOptions ) error {
156
189
157
190
if options != nil {
158
191
withClause , marshalErr := JSONMarshal (options )
@@ -162,23 +195,16 @@ func createIndex(ctx context.Context, store N1QLStore, indexName string, createS
162
195
createStatement = fmt .Sprintf (`%s with %s` , createStatement , withClause )
163
196
}
164
197
165
- DebugfCtx (ctx , KeyQuery , "Attempting to create index using statement: [%s]" , UD (createStatement ))
198
+ TracefCtx (ctx , KeyQuery , "Attempting to create index %q using statement: [%s]" , indexName , UD (createStatement ))
166
199
167
200
err := store .executeStatement (createStatement )
168
201
if err == nil {
169
202
return nil
170
203
}
171
204
172
- if IsIndexerRetryIndexError (err ) {
173
- InfofCtx (ctx , KeyQuery , "Indexer error creating index - waiting for server background retry. Error:%v" , err )
174
- // Wait for bucket to be created in background before returning
175
- return waitForIndexExistence (ctx , store , indexName , true )
176
- }
177
-
178
- if IsCreateDuplicateIndexError (err ) {
179
- InfofCtx (ctx , KeyQuery , "Duplicate index creation in progress - waiting for index readiness. Error:%v" , err )
180
- // Wait for bucket to be created in background before returning
181
- return waitForIndexExistence (ctx , store , indexName , true )
205
+ if IsIndexerRetryIndexError (err ) || IsCreateDuplicateIndexError (err ) {
206
+ DebugfCtx (ctx , KeyQuery , "Index %q is already being created on server: %v" , indexName , err )
207
+ return fmt .Errorf ("%w: %s" , ErrIndexBackgroundRetry , err .Error ())
182
208
}
183
209
184
210
return pkgerrors .WithStack (RedactErrorf ("Error creating index with statement: %s. Error: %v" , UD (createStatement ), err ))
@@ -211,56 +237,35 @@ func waitForIndexExistence(ctx context.Context, store N1QLStore, indexName strin
211
237
return nil
212
238
}
213
239
214
- // BuildDeferredIndexes issues a build command for any deferred sync gateway indexes associated with the bucket .
240
+ // BuildDeferredIndexes issues a build command for any deferred sync gateway indexes associated with the N1QLStore keyspace .
215
241
func BuildDeferredIndexes (ctx context.Context , s N1QLStore , indexSet []string ) error {
216
-
217
242
if len (indexSet ) == 0 {
218
243
return nil
219
244
}
220
245
221
- // Only build indexes that are in deferred state. Query system:indexes to validate the provided set of indexes
222
- statement := fmt .Sprintf ("SELECT indexes.name, indexes.state FROM system:indexes WHERE indexes.keyspace_id = '%s'" , s .IndexMetaKeyspaceID ())
246
+ InfofCtx (ctx , KeyQuery , "Building deferred indexes: %v" , indexSet )
223
247
224
- if s .IndexMetaBucketID () != "" {
225
- statement += fmt .Sprintf ("AND indexes.bucket_id = '%s' " , s .IndexMetaBucketID ())
226
- }
227
- if s .IndexMetaScopeID () != "" {
228
- statement += fmt .Sprintf ("AND indexes.scope_id = '%s' " , s .IndexMetaScopeID ())
248
+ // the provided indexes can be in a state that is not yet ready to take a build command
249
+ // there's a delay between the time of index creation and when it's actually found in the system:indexes table
250
+ // this results in buildIndexes returning a not found error for an index that was very recently created
251
+ worker := func () (shouldRetry bool , err error , value interface {}) {
252
+ err = buildIndexes (ctx , s , indexSet )
253
+ if IsIndexNotFoundError (err ) {
254
+ DebugfCtx (ctx , KeyQuery , "Index not found error when building indexes - will retry: %v" , err )
255
+ return true , err , nil
256
+ }
257
+ return err != nil , err , nil
229
258
}
230
-
231
- statement += fmt .Sprintf ("AND indexes.name IN [%s]" , StringSliceToN1QLArray (indexSet , "'" ))
232
- // mod: bucket name
233
-
234
- results , err := s .executeQuery (statement )
259
+ sleeper := CreateDoublingSleeperDurationFunc (500 , time .Second * 30 )
260
+ err , _ := RetryLoop (ctx , "BuildDeferredIndexes" , worker , sleeper )
235
261
if err != nil {
236
262
return err
237
263
}
238
- deferredIndexes := make ([]string , 0 )
239
- var indexInfo struct {
240
- Name string `json:"name"`
241
- State string `json:"state"`
242
- }
243
- for results .Next (ctx , & indexInfo ) {
244
- // If index is deferred (not built), add to set of deferred indexes
245
- if indexInfo .State == IndexStateDeferred {
246
- deferredIndexes = append (deferredIndexes , indexInfo .Name )
247
- }
248
- }
249
- closeErr := results .Close ()
250
- if closeErr != nil {
251
- return closeErr
252
- }
253
-
254
- if len (deferredIndexes ) == 0 {
255
- return nil
256
- }
257
264
258
- InfofCtx (ctx , KeyQuery , "Building deferred indexes: %v" , deferredIndexes )
259
- buildErr := buildIndexes (ctx , s , deferredIndexes )
260
- return buildErr
265
+ return nil
261
266
}
262
267
263
- // BuildIndexes executes a BUILD INDEX statement in the current bucket , using the form:
268
+ // BuildIndexes executes a BUILD INDEX statement in the N1QLStore keyspace , using the form:
264
269
//
265
270
// BUILD INDEX ON `bucket.Name`(`index1`, `index2`, ...)
266
271
func buildIndexes (ctx context.Context , s N1QLStore , indexNames []string ) error {
@@ -351,7 +356,7 @@ func getIndexMetaWithoutRetry(ctx context.Context, store N1QLStore, indexName st
351
356
return true , indexInfo , nil
352
357
}
353
358
354
- // DropIndex drops the specified index from the current bucket .
359
+ // DropIndex drops the specified index from the N1QLStore keyspace .
355
360
func DropIndex (ctx context.Context , store N1QLStore , indexName string ) error {
356
361
statement := fmt .Sprintf ("DROP INDEX default:%s.`%s`" , store .EscapedKeyspace (), indexName )
357
362
@@ -369,7 +374,7 @@ func DropIndex(ctx context.Context, store N1QLStore, indexName string) error {
369
374
return err
370
375
}
371
376
372
- // AsN1QLStore tries to return the given DataStore as a N1QLStore, based on underlying buckets .
377
+ // AsN1QLStore tries to return the given DataStore as a N1QLStore.
373
378
func AsN1QLStore (dataStore DataStore ) (N1QLStore , bool ) {
374
379
375
380
switch typedDataStore := dataStore .(type ) {
@@ -378,7 +383,7 @@ func AsN1QLStore(dataStore DataStore) (N1QLStore, bool) {
378
383
case * LeakyDataStore :
379
384
return typedDataStore , true
380
385
default :
381
- // bail out for unrecognised/unsupported buckets
386
+ // bail out for unrecognised/unsupported data store types
382
387
return nil , false
383
388
}
384
389
}
@@ -389,6 +394,9 @@ func AsN1QLStore(dataStore DataStore) (N1QLStore, bool) {
389
394
//
390
395
// Stuck with doing a string compare to differentiate between 'not found' and other errors
391
396
func IsIndexNotFoundError (err error ) bool {
397
+ if err == nil {
398
+ return false
399
+ }
392
400
return strings .Contains (err .Error (), "not found" )
393
401
}
394
402
@@ -425,6 +433,13 @@ func IsIndexerRetryBuildError(err error) bool {
425
433
return false
426
434
}
427
435
436
+ func IsIndexAlreadyExistsError (err error ) bool {
437
+ if err == nil {
438
+ return false
439
+ }
440
+ return strings .Contains (err .Error (), "already exists" )
441
+ }
442
+
428
443
// Check for transient indexer errors (can be retried)
429
444
func isTransientIndexerError (err error ) bool {
430
445
if err == nil {
@@ -594,7 +609,7 @@ func WaitForIndexesOnline(ctx context.Context, keyspace string, mgr *indexManage
594
609
if watchedOnlineIndexCount == len (indexNames ) {
595
610
return false , nil , nil
596
611
}
597
- InfofCtx (ctx , KeyAll , "Indexes %s not ready - retrying..." , strings .Join (offlineIndexes , ", " ))
612
+ DebugfCtx (ctx , KeyAll , "Indexes %s not ready - retrying..." , strings .Join (offlineIndexes , ", " ))
598
613
return true , nil , nil
599
614
}, retrySleeper )
600
615
return err
0 commit comments