11import type { Changes , SQLQueryBindings } from 'bun:sqlite'
22import type { AppendEventInput , AuthorizationGrant , AuthorizationScope , AuthorizationScopeType , AuthorizationTarget , CompactResult , ControlPlaneActor , ControlPlaneEnvironment , ControlPlaneEvent , ControlPlaneHealth , ControlPlaneOperation , ControlPlaneOrganization , ControlPlaneProject , ControlPlaneResource , ControlPlaneSnapshot , ControlPlaneStoreOptions , ControlPlaneTag , CreateActorInput , CreateEnvironmentInput , CreateGrantInput , CreateInvitationInput , CreateMembershipInput , CreateOperationInput , CreateOrganizationInput , CreateProjectInput , CreateResourceInput , EventListOptions , ImportSnapshotOptions , JsonValue , NavigationPreference , OperationListOptions , OperationState , OrganizationInvitation , OrganizationMembership , ReconcileResult , SavedFilter , TransitionOperationInput , UpdateProjectInput , UpdateResourceInput } from './types'
33import { createHash , randomBytes } from 'node:crypto'
4- import { chmodSync , existsSync , mkdirSync , statSync , writeFileSync } from 'node:fs'
4+ import { chmodSync , existsSync , mkdirSync , statSync } from 'node:fs'
55import { dirname , join } from 'node:path'
66import { resolveStatePath , statePath } from '@ts-cloud/core'
77import { Database } from 'bun:sqlite'
@@ -349,6 +349,16 @@ export class ControlPlaneStore {
349349 }
350350 }
351351
352+ private backupTo ( path : string ) : void {
353+ // SQLite writes VACUUM INTO incrementally. database.serialize() duplicates
354+ // the complete database in the Bun heap, which makes a routine schema
355+ // migration consume hundreds of megabytes once telemetry history grows.
356+ this . database . run ( 'VACUUM INTO ?' , [ path ] )
357+ try {
358+ chmodSync ( path , 0o600 )
359+ } catch { }
360+ }
361+
352362 private migrate ( ) : void {
353363 const row = this . database . query < Row , [ ] > ( 'PRAGMA user_version' ) . get ( )
354364 const current = Number ( row ?. user_version ?? 0 )
@@ -359,7 +369,7 @@ export class ControlPlaneStore {
359369 let backupPath : string | undefined
360370 if ( current > 0 && this . path !== ':memory:' ) {
361371 backupPath = `${ this . path } .v${ current } .${ Date . now ( ) } .bak`
362- writeFileSync ( backupPath , this . database . serialize ( ) , { mode : 0o600 } )
372+ this . backupTo ( backupPath )
363373 }
364374
365375 try {
@@ -723,7 +733,7 @@ export class ControlPlaneStore {
723733 }
724734
725735 listMemberships ( organizationId : string , options : { includeRevoked ?: boolean } = { } ) : OrganizationMembership [ ] {
726- const revoked = options . includeRevoked ? '' : " AND status = 'active'"
736+ const revoked = options . includeRevoked ? '' : ` AND status = 'active'`
727737 return this . database
728738 . query < Row , [ string ] > (
729739 `SELECT * FROM organization_memberships WHERE organization_id = ? ${ revoked } ORDER BY created_at, id` ,
@@ -802,7 +812,7 @@ export class ControlPlaneStore {
802812 const owners = Number (
803813 this . database
804814 . query < Row , [ string , string ] > (
805- " SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'" ,
815+ ` SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'` ,
806816 )
807817 . get ( current . organizationId , 'active' ) ?. count ?? 0 ,
808818 )
@@ -1535,7 +1545,7 @@ export class ControlPlaneStore {
15351545 createBackup ( reason : string = 'manual' ) : string {
15361546 if ( this . path === ':memory:' ) throw new Error ( 'Cannot create a filesystem backup for an in-memory control plane' )
15371547 const backupPath = `${ this . path } .${ Date . now ( ) } .bak`
1538- writeFileSync ( backupPath , this . database . serialize ( ) , { mode : 0o600 } )
1548+ this . backupTo ( backupPath )
15391549 this . setSetting ( 'storage.last_backup' , { path : backupPath , createdAt : this . now ( ) , reason } )
15401550 return backupPath
15411551 }
0 commit comments