Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export class SqliteCacheStore implements PersistentCacheStore<unknown> {
this.#db.exec(
'CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value BLOB, last_accessed INTEGER NOT NULL) WITHOUT ROWID;',
);
this.#db.exec(
'CREATE INDEX IF NOT EXISTS idx_cache_accessed ON cache (last_accessed DESC, key DESC);',
);

this.#getStmt = this.#db.prepare('SELECT value FROM cache WHERE key = ?');
this.#hasStmt = this.#db.prepare('SELECT 1 FROM cache WHERE key = ?');
Expand Down Expand Up @@ -143,22 +146,45 @@ export class SqliteCacheStore implements PersistentCacheStore<unknown> {
// Flush any pending access updates in one transaction before pruning
this.#flushAccessUpdates();

// 1. Delete items older than N days
this.#db
.prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);")
.run(`-${this.ttlDays} days`);

// 2. Prune oldest items if payload exceeds maxPayloadSize
const pruneStmt = this.#db.prepare(`
DELETE FROM cache WHERE key IN (
SELECT key FROM (
SELECT key,
sum(length(key) + length(value)) OVER (ORDER BY last_accessed DESC, key DESC) as running_size
FROM cache
) WHERE running_size > ?
);
`);
pruneStmt.run(this.maxPayloadSize);
this.#db.exec('BEGIN IMMEDIATE TRANSACTION;');
try {
// 1. Delete items older than N days
this.#db
.prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);")
.run(`-${this.ttlDays} days`);

// 2. Prune oldest items if payload exceeds maxPayloadSize
// Skip the expensive window aggregate query if total database size is below maxPayloadSize
const sizeResult = this.#db
.prepare(
'SELECT (page_count - freelist_count) * page_size AS total_size ' +
'FROM pragma_page_count(), pragma_freelist_count(), pragma_page_size();',
)
.get() as { total_size?: number } | undefined;

if ((sizeResult?.total_size ?? 0) > this.maxPayloadSize) {
this.#db
.prepare(
`DELETE FROM cache WHERE key IN (
SELECT key FROM (
SELECT key,
sum(length(key) + length(value)) OVER (ORDER BY last_accessed DESC, key DESC) as running_size
FROM cache
) WHERE running_size > ?
);`,
)
.run(this.maxPayloadSize);
}

this.#db.exec('COMMIT;');
} catch (error) {
try {
this.#db.exec('ROLLBACK;');
} catch {
// Ignore rollback errors if transaction was not active
}
throw error;
}
} catch {
// Pruning errors should not block build success
} finally {
Expand All @@ -173,7 +199,11 @@ export class SqliteCacheStore implements PersistentCacheStore<unknown> {
this.#setStmt = undefined;
this.#updateAccessedStmt = undefined;

this.#db.close();
try {
this.#db.close();
} catch {
// Failure to close should not block build success
}
this.#db = undefined;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,39 @@ describe('SqliteCacheStore', () => {
checkStore.close();
});

it('should not prune items when total database size is within maxPayloadSize on close', async () => {
store.close();

const sizeStore = new SqliteCacheStore(cachePath, 1024 * 1024);
await sizeStore.set('k1', 'value1');
await sizeStore.set('k2', 'value2');
sizeStore.close();

const checkStore = new SqliteCacheStore(cachePath);
expect(checkStore.has('k1')).toBeTrue();
expect(checkStore.has('k2')).toBeTrue();
checkStore.close();
});

it('should create an index on last_accessed and key', async () => {
// Trigger db initialization
await store.set('test-key', 'test-value');
store.close();

const { DatabaseSync } = await import('node:sqlite');
const directDb = new DatabaseSync(cachePath);
try {
const indexRows = directDb
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'cache' AND name = 'idx_cache_accessed'",
)
.all();
expect(indexRows.length).toBe(1);
} finally {
directDb.close();
}
});

describe('NG_BUILD_CACHE_STORE env variable option', () => {
it('should force SQLite when NG_BUILD_CACHE_STORE=sqlite', () => {
const code = `
Expand Down