Skip to content

Commit fc03c88

Browse files
committed
improvement(db): contract workspace file sizes
1 parent 1b597b0 commit fc03c88

19 files changed

Lines changed: 20190 additions & 346 deletions

.github/workflows/migrations.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ jobs:
8080
echo "ERROR: db:push needs an interactive rename decision; land it as a versioned migration instead of relying on push." >&2
8181
exit 1
8282
fi
83-
bun run ./scripts/apply-dev-workspace-file-size-cutover.ts
8483
else
8584
echo "Applying versioned migrations (db:migrate)"
8685
bun run ./scripts/migrate.ts

apps/sim/background/cleanup-soft-deletes.test.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -198,26 +198,6 @@ describe('cleanup soft deletes', () => {
198198
expect(mockDecrementStorageUsageForBillingContextInTx).not.toHaveBeenCalled()
199199
})
200200

201-
it('fails before deleting storage when canonical size metadata is missing', async () => {
202-
mockSelectRowsByIdChunks
203-
.mockResolvedValueOnce([])
204-
.mockResolvedValueOnce([])
205-
.mockResolvedValueOnce([
206-
{
207-
id: 'file-missing-size',
208-
key: 'workspace/ws-1/file-missing-size',
209-
workspaceId: 'ws-1',
210-
context: 'workspace',
211-
sizeBytes: null,
212-
},
213-
])
214-
215-
await expect(runCleanupSoftDeletes(basePayload)).rejects.toThrow(
216-
'Workspace file is missing canonical size_bytes metadata'
217-
)
218-
expect(mockDeleteFiles).not.toHaveBeenCalled()
219-
})
220-
221201
it('hard-deletes retained documents before deleting an expired knowledge base', async () => {
222202
mockChunkedBatchDelete.mockImplementationOnce(
223203
async (options: {

apps/sim/ee/workspace-forking/lib/copy/storage-quota.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
} from '@/ee/workspace-forking/lib/copy/storage-quota'
5050
import { ForkError } from '@/ee/workspace-forking/lib/lineage/authz'
5151

52-
function makeExecutor(total: number | string | null) {
52+
function makeExecutor(total: number | string) {
5353
const execute = vi.fn((_query: unknown) => Promise.resolve([{ total }]))
5454
return { executor: { execute } as unknown as DbOrTx, execute }
5555
}
@@ -71,8 +71,7 @@ describe('sumForkCopyBytes', () => {
7171
const compiled = outerQuery.toSQL()
7272
expect(compiled.sql).toBe('SELECT (? + ?)::bigint AS total')
7373
const [fileBytes, kbBytes] = compiled.params
74-
expect(fileBytes.toSQL().sql).toContain('count(*) FILTER')
75-
expect(fileBytes.toSQL().sql).toContain('IS NULL')
74+
expect(fileBytes.toSQL().sql).toContain('coalesce(sum')
7675
expect(fileBytes.toSQL().params).toContainEqual({
7776
type: 'and',
7877
conditions: [
@@ -103,17 +102,6 @@ describe('sumForkCopyBytes', () => {
103102
expect(bytes).toBe(1024)
104103
})
105104

106-
it('fails closed when a selected workspace file lacks canonical size metadata', async () => {
107-
const { executor } = makeExecutor(null)
108-
109-
await expect(
110-
sumForkCopyBytes(executor, 'src-ws', { fileIds: ['wf-missing-size'] })
111-
).rejects.toMatchObject({
112-
message: 'Storage calculation is temporarily unavailable',
113-
statusCode: 503,
114-
})
115-
})
116-
117105
it('runs no query for an empty selection', async () => {
118106
const { executor, execute } = makeExecutor(0)
119107

apps/sim/ee/workspace-forking/lib/copy/storage-quota.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,8 @@ export async function sumForkCopyBytes(
4747
const fileBytes =
4848
fileSelectors.length === 0
4949
? sql<number>`0`
50-
: sql<number | null>`(
51-
SELECT CASE
52-
WHEN count(*) FILTER (WHERE ${workspaceFiles.sizeBytes} IS NULL) > 0 THEN NULL
53-
ELSE coalesce(sum(${workspaceFiles.sizeBytes}), 0)
54-
END
50+
: sql<number>`(
51+
SELECT coalesce(sum(${workspaceFiles.sizeBytes}), 0)
5552
FROM ${workspaceFiles}
5653
WHERE ${and(
5754
fileSelectors.length === 1 ? fileSelectors[0] : or(...fileSelectors),
@@ -77,10 +74,10 @@ export async function sumForkCopyBytes(
7774
isNotNull(document.storageKey)
7875
)}
7976
)`
80-
const [row] = await executor.execute<{ total: number | string | null }>(
77+
const [row] = await executor.execute<{ total: number | string }>(
8178
sql`SELECT (${fileBytes} + ${kbBytes})::bigint AS total`
8279
)
83-
if (row?.total == null) {
80+
if (!row) {
8481
throw new ForkError('Storage calculation is temporarily unavailable', 503)
8582
}
8683
return Number(row.total)

apps/sim/lib/billing/storage/payer-transfer.test.ts

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ interface FakeTransferState {
102102
users?: Record<string, number>
103103
workspace: FakeWorkspace
104104
workspaceFileBytes?: number
105-
workspaceFileMissingSizeCount?: number
106105
}
107106

108107
function createFakeTx(state: FakeTransferState) {
@@ -166,7 +165,6 @@ function createFakeTx(state: FakeTransferState) {
166165
{
167166
document_bytes: state.documentBytes ?? 0,
168167
workspace_file_bytes: state.workspaceFileBytes ?? 0,
169-
workspace_file_missing_size_count: state.workspaceFileMissingSizeCount ?? 0,
170168
},
171169
]
172170
})
@@ -188,7 +186,6 @@ function updateFor(
188186

189187
interface FakeBatchTransferState {
190188
exactBytes: Record<string, number>
191-
missingSizeCounts?: Record<string, number>
192189
organizations?: Record<string, number>
193190
users?: Record<string, number>
194191
workspaces: FakeWorkspace[]
@@ -217,7 +214,6 @@ function createFakeBatchTx(state: FakeBatchTransferState) {
217214
workspace_id: workspaceId,
218215
document_bytes: 0,
219216
workspace_file_bytes: bytes,
220-
workspace_file_missing_size_count: state.missingSizeCounts?.[workspaceId] ?? 0,
221217
}))
222218
)
223219

@@ -535,31 +531,7 @@ describe('changeWorkspaceStoragePayerInTx', () => {
535531
expect(query.values).not.toContain('workspaceFiles.deletedAt')
536532
expect(query.values).toContain('document.connectorId')
537533
expect(query.values).toContain('document.deletedAt')
538-
expect(query.values.filter((value) => value === 'workspace-1')).toHaveLength(3)
539-
})
540-
541-
it('fails closed when a billable file is missing canonical size metadata', async () => {
542-
const fake = createFakeTx({
543-
workspace: {
544-
id: 'workspace-1',
545-
billedAccountUserId: 'user-1',
546-
organizationId: null,
547-
storageUsedBytes: 10,
548-
},
549-
workspaceFileBytes: 10,
550-
workspaceFileMissingSizeCount: 1,
551-
users: { 'user-1': 10, 'user-2': 0 },
552-
})
553-
554-
await expect(
555-
changeWorkspaceStoragePayerInTx(fake.tx, {
556-
workspaceId: 'workspace-1',
557-
organizationId: null,
558-
billedAccountUserId: 'user-2',
559-
})
560-
).rejects.toThrow('Workspace workspace-1 has files missing canonical size_bytes metadata')
561-
562-
expect(fake.updates).toEqual([])
534+
expect(query.values.filter((value) => value === 'workspace-1')).toHaveLength(2)
563535
})
564536
})
565537

@@ -712,34 +684,6 @@ describe('changeWorkspaceStoragePayersInTx', () => {
712684
expect(fake.updates).toEqual([])
713685
expect(fake.locks).toEqual([{ ids: ['workspace-a'], table: 'workspace' }])
714686
})
715-
716-
it('fails the batch before payer writes when canonical size metadata is missing', async () => {
717-
const fake = createFakeBatchTx({
718-
exactBytes: { 'workspace-a': 10 },
719-
missingSizeCounts: { 'workspace-a': 1 },
720-
users: { current: 10, destination: 0 },
721-
workspaces: [
722-
{
723-
id: 'workspace-a',
724-
billedAccountUserId: 'current',
725-
organizationId: null,
726-
storageUsedBytes: 10,
727-
},
728-
],
729-
})
730-
731-
await expect(
732-
changeWorkspaceStoragePayersInTx(fake.tx, [
733-
{
734-
workspaceId: 'workspace-a',
735-
organizationId: null,
736-
billedAccountUserId: 'destination',
737-
},
738-
])
739-
).rejects.toThrow('Workspace workspace-a has files missing canonical size_bytes metadata')
740-
741-
expect(fake.updates).toEqual([])
742-
})
743687
})
744688

745689
describe('changeOrganizationWorkspaceBilledAccountsInTx', () => {

apps/sim/lib/billing/storage/payer-transfer.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ interface ExactWorkspaceStorageRow {
1717
[key: string]: unknown
1818
document_bytes: number | string
1919
workspace_file_bytes: number | string
20-
workspace_file_missing_size_count: number | string
2120
}
2221

2322
interface BatchExactWorkspaceStorageRow extends ExactWorkspaceStorageRow {
@@ -88,13 +87,6 @@ async function getExactWorkspaceStorageBytes(tx: DbOrTx, workspaceId: string): P
8887
WHERE ${workspaceFiles.workspaceId} = ${workspaceId}
8988
AND ${workspaceFiles.context} = 'workspace'
9089
), 0)::bigint AS workspace_file_bytes,
91-
(
92-
SELECT COUNT(*)
93-
FROM ${workspaceFiles}
94-
WHERE ${workspaceFiles.workspaceId} = ${workspaceId}
95-
AND ${workspaceFiles.context} = 'workspace'
96-
AND ${workspaceFiles.sizeBytes} IS NULL
97-
)::bigint AS workspace_file_missing_size_count,
9890
COALESCE((
9991
SELECT SUM(${document.fileSize}::bigint)
10092
FROM ${document}
@@ -109,10 +101,6 @@ async function getExactWorkspaceStorageBytes(tx: DbOrTx, workspaceId: string): P
109101
if (!row) {
110102
throw new Error(`Could not recompute storage for workspace ${workspaceId}`)
111103
}
112-
if (parseExactBytes(row.workspace_file_missing_size_count, 'missing workspace file size') > 0) {
113-
throw new Error(`Workspace ${workspaceId} has files missing canonical size_bytes metadata`)
114-
}
115-
116104
const workspaceFileBytes = parseExactBytes(row.workspace_file_bytes, 'workspace file')
117105
const documentBytes = parseExactBytes(row.document_bytes, 'knowledge document')
118106
const total = workspaceFileBytes + documentBytes
@@ -178,16 +166,12 @@ async function getExactWorkspaceStorageBytesBatch(
178166
COALESCE(SUM(storage_by_workspace.workspace_file_bytes), 0)::bigint
179167
AS workspace_file_bytes,
180168
COALESCE(SUM(storage_by_workspace.document_bytes), 0)::bigint
181-
AS document_bytes,
182-
COALESCE(SUM(storage_by_workspace.workspace_file_missing_size_count), 0)::bigint
183-
AS workspace_file_missing_size_count
169+
AS document_bytes
184170
FROM (
185171
SELECT
186172
${workspaceFiles.workspaceId} AS workspace_id,
187173
SUM(${workspaceFiles.sizeBytes}) AS workspace_file_bytes,
188-
0::bigint AS document_bytes,
189-
COUNT(*) FILTER (WHERE ${workspaceFiles.sizeBytes} IS NULL)::bigint
190-
AS workspace_file_missing_size_count
174+
0::bigint AS document_bytes
191175
FROM ${workspaceFiles}
192176
WHERE ${inArray(workspaceFiles.workspaceId, workspaceIds)}
193177
AND ${workspaceFiles.context} = 'workspace'
@@ -198,8 +182,7 @@ async function getExactWorkspaceStorageBytesBatch(
198182
SELECT
199183
${knowledgeBase.workspaceId} AS workspace_id,
200184
0::bigint AS workspace_file_bytes,
201-
SUM(${document.fileSize}::bigint) AS document_bytes,
202-
0::bigint AS workspace_file_missing_size_count
185+
SUM(${document.fileSize}::bigint) AS document_bytes
203186
FROM ${document}
204187
INNER JOIN ${knowledgeBase}
205188
ON ${knowledgeBase.id} = ${document.knowledgeBaseId}
@@ -213,11 +196,6 @@ async function getExactWorkspaceStorageBytesBatch(
213196
`)
214197

215198
for (const row of rows) {
216-
if (parseExactBytes(row.workspace_file_missing_size_count, 'missing workspace file size') > 0) {
217-
throw new Error(
218-
`Workspace ${row.workspace_id} has files missing canonical size_bytes metadata`
219-
)
220-
}
221199
const workspaceFileBytes = parseExactBytes(row.workspace_file_bytes, 'workspace file')
222200
const documentBytes = parseExactBytes(row.document_bytes, 'knowledge document')
223201
const total = workspaceFileBytes + documentBytes

apps/sim/lib/uploads/shared/types.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,8 @@
55
*/
66
export const MAX_WORKSPACE_FILE_SIZE = 5 * 1024 * 1024 * 1024
77

8-
/**
9-
* Returns the canonical workspace-file byte size after the `size_bytes` cutover.
10-
*
11-
* The migration backfills every existing row before the new application image is
12-
* promoted, and its compatibility trigger fills the column for writes from an old
13-
* image during rollout. A null therefore indicates migration drift, not a legacy row.
14-
*/
15-
export function getWorkspaceFileSize(file: { sizeBytes: number | null }): number {
16-
if (file.sizeBytes === null) {
17-
throw new Error('Workspace file is missing canonical size_bytes metadata')
18-
}
8+
/** Returns a validated workspace-file byte size. */
9+
export function getWorkspaceFileSize(file: { sizeBytes: number }): number {
1910
if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0) {
2011
throw new Error(`Invalid workspace file size: ${file.sizeBytes}`)
2112
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
ALTER TABLE "workspace_files"
2+
ADD CONSTRAINT "workspace_files_size_bytes_not_null_check"
3+
CHECK ("size_bytes" IS NOT NULL) NOT VALID;--> statement-breakpoint
4+
ALTER TABLE "workspace_files"
5+
VALIDATE CONSTRAINT "workspace_files_size_bytes_not_null_check";--> statement-breakpoint
6+
-- migration-safe: contract of #7112 and #7123 — application reads and writes use size_bytes, the backfill is complete, and this PR must merge only after the compatibility release fully drains
7+
ALTER TABLE "workspace_files" ALTER COLUMN "size_bytes" SET NOT NULL;--> statement-breakpoint
8+
-- migration-safe: removes the temporary proof constraint created and validated above after PostgreSQL records the equivalent column-level NOT NULL invariant
9+
ALTER TABLE "workspace_files"
10+
DROP CONSTRAINT "workspace_files_size_bytes_not_null_check";--> statement-breakpoint
11+
DROP TRIGGER IF EXISTS "workspace_files_sync_size_columns" ON "workspace_files";--> statement-breakpoint
12+
DROP FUNCTION IF EXISTS "sync_workspace_file_size_columns"();--> statement-breakpoint
13+
-- migration-safe: contract of #7112 and #7123 — no deployed application reader or writer depends on size, and this PR must merge only after the compatibility release fully drains
14+
ALTER TABLE "workspace_files" DROP COLUMN "size";

0 commit comments

Comments
 (0)