• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage / 19098571719

05 Nov 2025 10:14AM UTC coverage: 77.755% (+1.4%) from 76.389%
19098571719

Pull #774

github

web-flow
Merge b7a491e98 into 76df298f8
Pull Request #774: feat: vector buckets

2024 of 2927 branches covered (69.15%)

Branch coverage included in aggregate %.

3421 of 3928 new or added lines in 58 files covered. (87.09%)

29 existing lines in 2 files now uncovered.

24583 of 31292 relevant lines covered (78.56%)

91.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.12
/src/storage/protocols/vector/vector-store.ts
1
import {
1✔
2
  CreateIndexInput,
1✔
3
  DeleteIndexInput,
1✔
4
  DistanceMetric,
1✔
5
  GetIndexCommandInput,
1✔
6
  ListIndexesInput,
1✔
7
  MetadataConfiguration,
1✔
8
  GetIndexOutput,
1✔
9
  PutVectorsInput,
1✔
10
  ListVectorsInput,
1✔
11
  ListVectorBucketsInput,
1✔
12
  QueryVectorsInput,
1✔
13
  DeleteVectorsInput,
1✔
14
  GetVectorBucketInput,
1✔
15
  GetVectorsCommandInput,
1✔
16
  ConflictException,
1✔
17
} from '@aws-sdk/client-s3vectors'
1✔
18
import { VectorMetadataDB } from './knex'
1✔
19
import { VectorStore } from './adapter/s3-vector'
1✔
20
import { ERRORS } from '@internal/errors'
1✔
21
import { Sharder } from '@internal/sharding/sharder'
1✔
22
import { logger, logSchema } from '@internal/monitoring'
1✔
23

1✔
24
interface VectorStoreConfig {
1✔
25
  tenantId: string
1✔
26
  maxBucketCount: number
1✔
27
  maxIndexCount: number
1✔
28
}
1✔
29

1✔
30
export class VectorStoreManager {
1✔
31
  constructor(
1✔
32
    protected readonly vectorStore: VectorStore,
89✔
33
    protected readonly db: VectorMetadataDB,
89✔
34
    protected readonly sharding: Sharder,
89✔
35
    protected readonly config: VectorStoreConfig
89✔
36
  ) {}
89✔
37

1✔
38
  protected getIndexName(name: string) {
1✔
39
    return `${this.config.tenantId}-${name}`
69✔
40
  }
69✔
41

1✔
42
  async createBucket(bucketName: string): Promise<void> {
1✔
43
    await this.db.withTransaction(
100✔
44
      async (tnx) => {
100✔
45
        const bucketCount = await tnx.countBuckets()
100✔
46
        if (bucketCount >= this.config.maxBucketCount) {
100!
NEW
47
          throw ERRORS.S3VectorMaxBucketsExceeded(this.config.maxBucketCount)
×
NEW
48
        }
×
49

100✔
50
        try {
100✔
51
          await tnx.createVectorBucket(bucketName)
100✔
52
        } catch (e) {
100✔
53
          if (e instanceof ConflictException) {
1!
NEW
54
            return
×
NEW
55
          }
×
56
          throw e
1✔
57
        }
1✔
58
      },
100✔
59
      { isolationLevel: 'serializable' }
100✔
60
    )
100✔
61
  }
99✔
62

1✔
63
  async deleteBucket(bucketName: string): Promise<void> {
1✔
64
    await this.db.withTransaction(
3✔
65
      async (tx) => {
3✔
66
        const indexes = await tx.listIndexes({ bucketId: bucketName, maxResults: 1 })
3✔
67

3✔
68
        if (indexes.indexes.length > 0) {
3✔
69
          throw ERRORS.S3VectorBucketNotEmpty(bucketName)
1✔
70
        }
1✔
71

2✔
72
        await tx.deleteVectorBucket(bucketName)
2✔
73
      },
3✔
74
      { isolationLevel: 'serializable' }
3✔
75
    )
3✔
76
  }
2✔
77

1✔
78
  async getBucket(command: GetVectorBucketInput) {
1✔
79
    if (!command.vectorBucketName) {
2!
NEW
80
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
81
    }
×
82

2✔
83
    const vectorBucket = await this.db.findVectorBucket(command.vectorBucketName)
2✔
84

1✔
85
    return {
1✔
86
      vectorBucket: {
1✔
87
        vectorBucketName: vectorBucket.id,
1✔
88
        creationTime: vectorBucket.created_at
1✔
89
          ? Math.floor(vectorBucket.created_at.getTime() / 1000)
1✔
90
          : undefined,
2!
91
      },
2✔
92
    }
2✔
93
  }
2✔
94

1✔
95
  async listBuckets(command: ListVectorBucketsInput) {
1✔
96
    const bucketResult = await this.db.listBuckets({
5✔
97
      maxResults: command.maxResults,
5✔
98
      nextToken: command.nextToken,
5✔
99
      prefix: command.prefix,
5✔
100
    })
5✔
101

5✔
102
    return {
5✔
103
      vectorBuckets: bucketResult.vectorBuckets.map((bucket) => ({
5✔
104
        vectorBucketName: bucket.id,
34✔
105
        creationTime: bucket.created_at
34✔
106
          ? Math.floor(bucket.created_at.getTime() / 1000)
34✔
107
          : undefined,
34!
108
      })),
5✔
109
      nextToken: bucketResult.nextToken,
5✔
110
    }
5✔
111
  }
5✔
112

1✔
113
  // Store it in MultiTenantDB
1✔
114
  // Queue Job in the same transaction
1✔
115
  // Poll for job completion
1✔
116
  async createVectorIndex(command: CreateIndexInput): Promise<void> {
1✔
117
    if (!command.indexName) {
56!
NEW
118
      throw ERRORS.MissingParameter('indexName')
×
NEW
119
    }
×
120

56✔
121
    if (!command.vectorBucketName) {
56!
NEW
122
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
123
    }
×
124

56✔
125
    await this.db.findVectorBucket(command.vectorBucketName)
56✔
126

56✔
127
    const createIndexInput = {
56✔
128
      ...command,
56✔
129
      indexName: this.getIndexName(command.indexName),
56✔
130
    }
56✔
131

56✔
132
    let shardReservation: { reservationId: string; shardKey: string; shardId: string } | undefined
56✔
133

56✔
134
    try {
56✔
135
      await this.db.withTransaction(
56✔
136
        async (tx) => {
56✔
137
          const indexCount = await tx.countIndexes(command.vectorBucketName!)
56✔
138

56✔
139
          if (indexCount >= this.config.maxIndexCount) {
56!
NEW
140
            throw ERRORS.S3VectorMaxIndexesExceeded(this.config.maxIndexCount)
×
NEW
141
          }
×
142

56✔
143
          await tx.createVectorIndex({
56✔
144
            dataType: createIndexInput.dataType!,
56✔
145
            dimension: createIndexInput.dimension!,
56✔
146
            distanceMetric: createIndexInput.distanceMetric!,
56✔
147
            indexName: command.indexName!,
56✔
148
            metadataConfiguration: createIndexInput.metadataConfiguration,
56✔
149
            vectorBucketName: command.vectorBucketName!,
56✔
150
          })
56✔
151

56✔
152
          shardReservation = await this.sharding.reserve({
56✔
153
            kind: 'vector',
56✔
154
            bucketName: command.vectorBucketName!,
56✔
155
            tenantId: this.config.tenantId,
56✔
156
            logicalName: command.indexName!,
56✔
157
          })
56✔
158

56✔
159
          if (!shardReservation) {
56!
NEW
160
            throw ERRORS.S3VectorNoAvailableShard()
×
NEW
161
          }
×
162

56✔
163
          try {
56✔
164
            if (
56✔
165
              createIndexInput.metadataConfiguration &&
56✔
166
              createIndexInput.metadataConfiguration.nonFilterableMetadataKeys &&
56✔
167
              createIndexInput.metadataConfiguration.nonFilterableMetadataKeys.length === 0
7✔
168
            ) {
56!
NEW
169
              delete createIndexInput.metadataConfiguration
×
NEW
170
            }
×
171

56✔
172
            await this.vectorStore.createVectorIndex({
56✔
173
              ...createIndexInput,
56✔
174
              vectorBucketName: shardReservation.shardKey,
56✔
175
            })
56✔
176

55✔
177
            await this.sharding.confirm(shardReservation.reservationId, {
55✔
178
              kind: 'vector',
55✔
179
              bucketName: command.vectorBucketName!,
55✔
180
              tenantId: this.config.tenantId,
55✔
181
              logicalName: command.indexName!,
55✔
182
            })
55✔
183
          } catch (e) {
56✔
184
            logSchema.error(logger, 'Vector index creation failed', {
1✔
185
              type: 'vector',
1✔
186
              error: e,
1✔
187
              project: this.config.tenantId,
1✔
188
            })
1✔
189
            if (e instanceof ConflictException) {
1!
NEW
190
              await this.sharding.confirm(shardReservation.reservationId, {
×
NEW
191
                kind: 'vector',
×
NEW
192
                bucketName: command.vectorBucketName!,
×
NEW
193
                tenantId: this.config.tenantId,
×
NEW
194
                logicalName: command.indexName!,
×
NEW
195
              })
×
NEW
196
              return
×
NEW
197
            }
×
198

1✔
199
            throw e
1✔
200
          }
1✔
201
        },
56✔
202
        { isolationLevel: 'serializable' }
56✔
203
      )
56✔
204
    } catch (error) {
56✔
205
      logSchema.error(logger, 'Create vector index transaction failed', {
1✔
206
        type: 'vector',
1✔
207
        error: error,
1✔
208
        project: this.config.tenantId,
1✔
209
      })
1✔
210
      if (shardReservation) {
1✔
211
        await this.sharding.cancel(shardReservation.reservationId)
1✔
212
      }
1✔
213
      throw error
1✔
214
    }
1✔
215
  }
56✔
216

1✔
217
  async deleteIndex(command: DeleteIndexInput): Promise<void> {
1✔
218
    if (!command.indexName) {
2!
NEW
219
      throw ERRORS.MissingParameter('indexName')
×
NEW
220
    }
×
221

2✔
222
    if (!command.vectorBucketName) {
2!
NEW
223
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
224
    }
×
225

2✔
226
    await this.db.findVectorIndexForBucket(command.vectorBucketName, command.indexName)
2✔
227

1✔
228
    const vectorIndexName = this.getIndexName(command.indexName)
1✔
229

1✔
230
    await this.db.withTransaction(async (tx) => {
1✔
231
      const shard = await this.sharding.findShardByResourceId({
1✔
232
        kind: 'vector',
1✔
233
        tenantId: this.config.tenantId,
1✔
234
        logicalName: command.indexName!,
1✔
235
        bucketName: command.vectorBucketName!,
1✔
236
      })
1✔
237

1✔
238
      if (!shard) {
1!
NEW
239
        throw ERRORS.S3VectorNoAvailableShard()
×
NEW
240
      }
×
241

1✔
242
      await tx.deleteVectorIndex(command.vectorBucketName!, command.indexName!)
1✔
243

1✔
244
      await this.sharding.freeByResource(shard.id, {
1✔
245
        kind: 'vector',
1✔
246
        tenantId: this.config.tenantId,
1✔
247
        bucketName: command.vectorBucketName!,
1✔
248
        logicalName: command.indexName!,
1✔
249
      })
1✔
250

1✔
251
      await this.vectorStore.deleteVectorIndex({
1✔
252
        vectorBucketName: shard.shard_key,
1✔
253
        indexName: vectorIndexName,
1✔
254
      })
1✔
255
    })
1✔
256
  }
1✔
257

1✔
258
  async getIndex(command: GetIndexCommandInput): Promise<GetIndexOutput> {
1✔
259
    if (!command.indexName) {
2!
NEW
260
      throw ERRORS.MissingParameter('indexName')
×
NEW
261
    }
×
262

2✔
263
    if (!command.vectorBucketName) {
2!
NEW
264
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
265
    }
×
266

2✔
267
    const index = await this.db.getIndex(command.vectorBucketName, command.indexName)
2✔
268

1✔
269
    return {
1✔
270
      index: {
1✔
271
        indexName: index.name,
1✔
272
        dataType: index.data_type as 'float32',
1✔
273
        dimension: index.dimension,
1✔
274
        distanceMetric: index.distance_metric as DistanceMetric,
1✔
275
        metadataConfiguration: index.metadata_configuration as MetadataConfiguration,
1✔
276
        vectorBucketName: index.bucket_id,
1✔
277
        creationTime: index.created_at,
1✔
278
        indexArn: undefined,
1✔
279
      },
1✔
280
    }
1✔
281
  }
1✔
282

1✔
283
  async listIndexes(command: ListIndexesInput) {
1✔
284
    if (!command.vectorBucketName) {
3!
NEW
285
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
286
    }
×
287

3✔
288
    const result = await this.db.listIndexes({
3✔
289
      bucketId: command.vectorBucketName,
3✔
290
      maxResults: command.maxResults,
3✔
291
      nextToken: command.nextToken,
3✔
292
      prefix: command.prefix,
3✔
293
    })
3✔
294

3✔
295
    return {
3✔
296
      indexes: result.indexes.map((i) => ({
3✔
297
        indexName: i.name,
4✔
298
        vectorBucketName: i.bucket_id,
4✔
299
        creationTime: Math.floor(i.created_at.getTime() / 1000),
4✔
300
      })),
3✔
301
    }
3✔
302
  }
3✔
303

1✔
304
  async putVectors(command: PutVectorsInput) {
1✔
305
    if (!command.indexName) {
2!
NEW
306
      throw ERRORS.MissingParameter('indexName')
×
NEW
307
    }
×
308

2✔
309
    if (!command.vectorBucketName) {
2!
NEW
310
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
311
    }
×
312

2✔
313
    const [shard] = await Promise.all([
2✔
314
      this.sharding.findShardByResourceId({
2✔
315
        kind: 'vector',
2✔
316
        tenantId: this.config.tenantId,
2✔
317
        logicalName: command.indexName!,
2✔
318
        bucketName: command.vectorBucketName!,
2✔
319
      }),
2✔
320
      this.db.findVectorIndexForBucket(command.vectorBucketName, command.indexName),
2✔
321
    ])
2✔
322

1✔
323
    if (!shard) {
2!
NEW
324
      throw ERRORS.S3VectorNoAvailableShard()
×
NEW
325
    }
×
326

1✔
327
    const putVectorsInput = {
1✔
328
      ...command,
1✔
329
      vectorBucketName: shard.shard_key,
1✔
330
      indexName: this.getIndexName(command.indexName),
1✔
331
    }
1✔
332
    await this.vectorStore.putVectors(putVectorsInput)
1✔
333
  }
1✔
334

1✔
335
  async deleteVectors(command: DeleteVectorsInput) {
1✔
336
    if (!command.indexName) {
2!
NEW
337
      throw ERRORS.MissingParameter('indexName')
×
NEW
338
    }
×
339

2✔
340
    if (!command.vectorBucketName) {
2!
NEW
341
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
342
    }
×
343

2✔
344
    const [shard] = await Promise.all([
2✔
345
      this.sharding.findShardByResourceId({
2✔
346
        kind: 'vector',
2✔
347
        tenantId: this.config.tenantId,
2✔
348
        logicalName: command.indexName!,
2✔
349
        bucketName: command.vectorBucketName!,
2✔
350
      }),
2✔
351
      this.db.findVectorIndexForBucket(command.vectorBucketName, command.indexName),
2✔
352
    ])
2✔
353

1✔
354
    if (!shard) {
2!
NEW
355
      throw ERRORS.S3VectorNoAvailableShard()
×
NEW
356
    }
×
357

1✔
358
    const deleteVectorsInput = {
1✔
359
      ...command,
1✔
360
      vectorBucketName: shard.shard_key,
1✔
361
      indexName: this.getIndexName(command.indexName),
1✔
362
    }
1✔
363

1✔
364
    return this.vectorStore.deleteVectors(deleteVectorsInput)
1✔
365
  }
1✔
366

1✔
367
  async listVectors(command: ListVectorsInput) {
1✔
368
    if (!command.indexName) {
6!
NEW
369
      throw ERRORS.MissingParameter('indexName')
×
NEW
370
    }
×
371

6✔
372
    if (!command.vectorBucketName) {
6!
NEW
373
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
374
    }
×
375

6✔
376
    const [shard] = await Promise.all([
6✔
377
      this.sharding.findShardByResourceId({
6✔
378
        kind: 'vector',
6✔
379
        tenantId: this.config.tenantId,
6✔
380
        logicalName: command.indexName!,
6✔
381
        bucketName: command.vectorBucketName!,
6✔
382
      }),
6✔
383
      this.db.findVectorIndexForBucket(command.vectorBucketName, command.indexName),
6✔
384
    ])
6✔
385

5✔
386
    if (!shard) {
6!
NEW
387
      throw ERRORS.S3VectorNoAvailableShard()
×
NEW
388
    }
×
389

5✔
390
    const listVectorsInput = {
5✔
391
      ...command,
5✔
392
      vectorBucketName: shard.shard_key,
5✔
393
      indexName: this.getIndexName(command.indexName),
5✔
394
    }
5✔
395

5✔
396
    const result = await this.vectorStore.listVectors(listVectorsInput)
5✔
397

5✔
398
    return {
5✔
399
      vectors: result.vectors,
5✔
400
      nextToken: result.nextToken,
5✔
401
    }
5✔
402
  }
5✔
403

1✔
404
  async queryVectors(command: QueryVectorsInput) {
1✔
405
    if (!command.indexName) {
4!
NEW
406
      throw ERRORS.MissingParameter('indexName')
×
NEW
407
    }
×
408

4✔
409
    if (!command.vectorBucketName) {
4!
NEW
410
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
411
    }
×
412

4✔
413
    const [shard] = await Promise.all([
4✔
414
      this.sharding.findShardByResourceId({
4✔
415
        kind: 'vector',
4✔
416
        tenantId: this.config.tenantId,
4✔
417
        logicalName: command.indexName!,
4✔
418
        bucketName: command.vectorBucketName!,
4✔
419
      }),
4✔
420
      this.db.findVectorIndexForBucket(command.vectorBucketName, command.indexName),
4✔
421
    ])
4✔
422

3✔
423
    if (!shard) {
4!
NEW
424
      throw ERRORS.S3VectorNoAvailableShard()
×
NEW
425
    }
×
426

3✔
427
    const queryInput = {
3✔
428
      ...command,
3✔
429
      vectorBucketName: shard.shard_key,
3✔
430
      indexName: this.getIndexName(command.indexName),
3✔
431
    }
3✔
432
    return this.vectorStore.queryVectors(queryInput)
3✔
433
  }
3✔
434

1✔
435
  async getVectors(command: GetVectorsCommandInput) {
1✔
436
    if (!command.indexName) {
3!
NEW
437
      throw ERRORS.MissingParameter('indexName')
×
NEW
438
    }
×
439

3✔
440
    if (!command.vectorBucketName) {
3!
NEW
441
      throw ERRORS.MissingParameter('vectorBucketName')
×
NEW
442
    }
×
443

3✔
444
    const [shard] = await Promise.all([
3✔
445
      this.sharding.findShardByResourceId({
3✔
446
        kind: 'vector',
3✔
447
        tenantId: this.config.tenantId,
3✔
448
        logicalName: command.indexName!,
3✔
449
        bucketName: command.vectorBucketName!,
3✔
450
      }),
3✔
451
      this.db.findVectorIndexForBucket(command.vectorBucketName, command.indexName),
3✔
452
    ])
3✔
453

2✔
454
    if (!shard) {
3!
NEW
455
      throw ERRORS.S3VectorNoAvailableShard()
×
NEW
456
    }
×
457

2✔
458
    const getVectorsInput = {
2✔
459
      ...command,
2✔
460
      vectorBucketName: shard.shard_key,
2✔
461
      indexName: this.getIndexName(command.indexName),
2✔
462
    }
2✔
463

2✔
464
    const result = await this.vectorStore.getVectors(getVectorsInput)
2✔
465

2✔
466
    return {
2✔
467
      vectors: result.vectors,
2✔
468
    }
2✔
469
  }
2✔
470
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc