Skip to content

Commit 24f0611

Browse files
committed
fix(knowledge): bound source titles and tag values so an oversized index row cannot fail a connector sync
1 parent cb7de98 commit 24f0611

2 files changed

Lines changed: 58 additions & 5 deletions

File tree

‎apps/sim/lib/knowledge/connectors/sync-persistence.test.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,19 @@ vi.mock('@/lib/knowledge/documents/storage-cleanup', () => ({
2727
}),
2828
isKnowledgeBaseOwnedStorageKey: (key: string) => key.startsWith('kb/'),
2929
}))
30-
vi.mock('@/connectors/registry.server', () => ({ CONNECTOR_REGISTRY: {} }))
30+
vi.mock('@/connectors/registry.server', () => ({
31+
CONNECTOR_REGISTRY: {
32+
fixture: { mapTags: (metadata: Record<string, unknown>) => ({ label: metadata.label }) },
33+
},
34+
}))
3135

3236
import { MAX_ACL_TOKENS } from '@/lib/knowledge/access/tokens'
3337
import { getConnectorFailureDiagnostic } from '@/lib/knowledge/connectors/connector-error'
3438
import {
3539
addDocument,
3640
persistDocumentAcls,
3741
persistSourceDocumentFailures,
42+
resolveTagMapping,
3843
} from '@/lib/knowledge/connectors/sync-persistence'
3944

4045
const CONNECTOR = 'connector-1'
@@ -350,6 +355,18 @@ describe('persistSourceDocumentFailures', () => {
350355
expect(JSON.stringify(dbChainMockFns.set.mock.calls)).not.toContain('private body')
351356
expect(dbChainMockFns.delete).not.toHaveBeenCalled()
352357
})
358+
it('bounds a source title that would exceed the filename index row limit', async () => {
359+
leaseHeld()
360+
const title = 'x'.repeat(5000)
361+
await persistSourceDocumentFailures({
362+
...input,
363+
documents: [{ ...input.documents[0], title }],
364+
priorByExternalId: new Map(),
365+
})
366+
const [rows] = dbChainMockFns.values.mock.calls[0] as [Array<{ filename: string }>]
367+
expect(rows[0].filename.length).toBeLessThan(600)
368+
expect(rows[0].filename.startsWith('xxxx')).toBe(true)
369+
})
353370
it('refuses to commit a failure under a reclaimed lease', async () => {
354371
queueTableRows(schemaMock.knowledgeBase, [{ id: 'kb' }])
355372
await expect(
@@ -416,3 +433,24 @@ describe('organization source cache persistence', () => {
416433
expect(mockUploadFile).not.toHaveBeenCalled()
417434
})
418435
})
436+
437+
describe('resolveTagMapping', () => {
438+
it('bounds a mapped tag value that would exceed its index row limit', () => {
439+
const tags = resolveTagMapping(
440+
'fixture',
441+
{ label: 'y'.repeat(5000) },
442+
{ tagSlotMapping: { label: 'tag1' } }
443+
)
444+
expect(tags?.tag1?.length).toBeLessThan(600)
445+
expect(tags?.tag1?.startsWith('yyyy')).toBe(true)
446+
})
447+
448+
it('keeps a short mapped tag value intact', () => {
449+
const tags = resolveTagMapping(
450+
'fixture',
451+
{ label: 'Purchasing' },
452+
{ tagSlotMapping: { label: 'tag1' } }
453+
)
454+
expect(tags?.tag1).toBe('Purchasing')
455+
})
456+
})

‎apps/sim/lib/knowledge/connectors/sync-persistence.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { document, embedding, knowledgeBase, knowledgeConnector } from '@sim/db/
33
import { createLogger } from '@sim/logger'
44
import { chunkArray } from '@sim/utils/helpers'
55
import { generateId } from '@sim/utils/id'
6+
import { truncate } from '@sim/utils/string'
67
import { and, eq, exists, inArray, isNull, lt, or, sql } from 'drizzle-orm'
78
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
89
import type { DbOrTx } from '@/lib/db/types'
@@ -173,6 +174,19 @@ export async function persistDocumentAcls(
173174

174175
const MAX_SAFE_TITLE_LENGTH = 200
175176

177+
/**
178+
* A document's filename and text tags sit under btree indexes, and Postgres refuses an index
179+
* row past about 2.7 KB. A source title or tag value beyond that fails the row and, with it,
180+
* every sync that lists the document again. 512 characters keeps a four-byte-per-character
181+
* value inside the ceiling.
182+
*/
183+
const MAX_INDEXED_TEXT_LENGTH = 512
184+
185+
/** Bounds a source-supplied value that lands in an indexed text column. */
186+
function boundIndexedText(value: string): string {
187+
return truncate(value, MAX_INDEXED_TEXT_LENGTH)
188+
}
189+
176190
function sanitizeStorageTitle(title: string): string {
177191
return title.replace(/[^a-zA-Z0-9.-]/g, '_').slice(0, MAX_SAFE_TITLE_LENGTH)
178192
}
@@ -250,7 +264,8 @@ export function resolveTagMapping(
250264
const result: Partial<DocumentTags> = {}
251265
for (const [semanticKey, slot] of Object.entries(mapping)) {
252266
const value = semanticTags[semanticKey]
253-
;(result as Record<string, unknown>)[slot] = value != null ? value : null
267+
;(result as Record<string, unknown>)[slot] =
268+
typeof value === 'string' ? boundIndexedText(value) : value != null ? value : null
254269
}
255270
return result
256271
}
@@ -278,7 +293,7 @@ function buildSkippedDocumentRow(
278293
return {
279294
id: generateId(),
280295
knowledgeBaseId,
281-
filename: extDoc.title,
296+
filename: boundIndexedText(extDoc.title),
282297
fileUrl: '',
283298
storageKey: null,
284299
/** No artifact was stored; a provider's reported source size is not local storage usage. */
@@ -630,7 +645,7 @@ export async function addDocument(
630645
await tx.insert(document).values({
631646
id: documentId,
632647
knowledgeBaseId,
633-
filename: extDoc.title,
648+
filename: boundIndexedText(extDoc.title),
634649
fileUrl,
635650
storageKey: fileInfo.key,
636651
fileSize: artifact.bytes.length,
@@ -745,7 +760,7 @@ export async function updateDocument(
745760
await tx
746761
.update(document)
747762
.set({
748-
filename: extDoc.title,
763+
filename: boundIndexedText(extDoc.title),
749764
fileUrl,
750765
storageKey: fileInfo.key,
751766
fileSize: artifact.bytes.length,

0 commit comments

Comments
 (0)