Repository navigation
feat(database): implement SQLite initialization and repository for directory state management #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
egalvis27
wants to merge
3
commits into
main
Choose a base branch
from
feat/prepare-database-for-best-sync
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
69 changes: 69 additions & 0 deletions
69
src/apps/main/database/initialize-virtual-drive-sqlite.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { calls } from 'tests/vitest/utils.helper'; | ||
| import { initializeVirtualDriveSqlite } from './initialize-virtual-drive-sqlite'; | ||
|
|
||
| describe('initialize-virtual-drive-sqlite', () => { | ||
| const queryMock = vi.fn<(statement: string) => Promise<unknown>>(); | ||
| let props: NonNullable<Parameters<typeof initializeVirtualDriveSqlite>[0]>; | ||
|
|
||
| beforeEach(() => { | ||
| queryMock.mockResolvedValue(undefined); | ||
| props = { | ||
| dataSource: { | ||
| isInitialized: true, | ||
| query: queryMock, | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| it('should skip the bootstrap when the data source is not initialized', async () => { | ||
| props.dataSource!.isInitialized = false; | ||
|
|
||
| await initializeVirtualDriveSqlite(props); | ||
|
|
||
| calls(queryMock).toHaveLength(0); | ||
| }); | ||
|
|
||
| it('should configure sqlite and create the directory state schema', async () => { | ||
| await initializeVirtualDriveSqlite(props); | ||
|
|
||
| calls(queryMock).toHaveLength(11); | ||
| expect(queryMock.mock.invocationCallOrder).toStrictEqual( | ||
| [...queryMock.mock.invocationCallOrder].sort((a, b) => a - b), | ||
| ); | ||
| expect(queryMock).toHaveBeenCalledWith('PRAGMA journal_mode=WAL;'); | ||
| expect(queryMock).not.toHaveBeenCalledWith('PRAGMA synchronous=NORMAL;'); | ||
| expect(queryMock).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS drive_directory_state')); | ||
| expect(queryMock.mock.calls[0]).toStrictEqual(['PRAGMA busy_timeout=5000;']); | ||
| expect(queryMock.mock.calls[1]).toStrictEqual(['PRAGMA journal_mode=WAL;']); | ||
| }); | ||
|
|
||
| it('should propagate WAL errors', async () => { | ||
| const error = new Error('database is unavailable'); | ||
| queryMock.mockImplementation((statement) => { | ||
| if (statement === 'PRAGMA journal_mode=WAL;') { | ||
| return Promise.reject(error); | ||
| } | ||
|
|
||
| return Promise.resolve(undefined); | ||
| }); | ||
|
|
||
| await expect(initializeVirtualDriveSqlite(props)).rejects.toBe(error); | ||
|
|
||
| calls(queryMock).toHaveLength(2); | ||
| }); | ||
|
|
||
| it('should propagate directory state schema errors', async () => { | ||
| const error = new Error('schema creation failed'); | ||
| queryMock.mockImplementation((statement) => { | ||
| if (statement.includes('CREATE TABLE IF NOT EXISTS drive_directory_state')) { | ||
| return Promise.reject(error); | ||
| } | ||
|
|
||
| return Promise.resolve(undefined); | ||
| }); | ||
|
|
||
| await expect(initializeVirtualDriveSqlite(props)).rejects.toBe(error); | ||
|
|
||
| expect(queryMock).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS drive_directory_state')); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { AppDataSource } from './data-source'; | ||
|
|
||
| type SqliteQueryExecutor = { | ||
| isInitialized: boolean; | ||
| query: (statement: string) => Promise<unknown>; | ||
| }; | ||
|
|
||
| type Props = { | ||
| dataSource?: SqliteQueryExecutor; | ||
| }; | ||
|
|
||
| const SQLITE_BOOTSTRAP_STATEMENTS = [ | ||
| 'PRAGMA busy_timeout=5000;', | ||
| 'PRAGMA journal_mode=WAL;', | ||
| 'PRAGMA temp_store=MEMORY;', | ||
| 'PRAGMA foreign_keys=ON;', | ||
| 'PRAGMA wal_autocheckpoint=1000;', | ||
| 'PRAGMA mmap_size=268435456;', | ||
| `CREATE TABLE IF NOT EXISTS drive_directory_state ( | ||
| folder_id INTEGER NOT NULL, | ||
| status_scope TEXT NOT NULL, | ||
| children_loaded_at TEXT NULL, | ||
| children_last_error_at TEXT NULL, | ||
| fetch_state TEXT NOT NULL DEFAULT 'idle', | ||
| PRIMARY KEY (folder_id, status_scope) | ||
| );`, | ||
| 'CREATE INDEX IF NOT EXISTS idx_drive_file_folder_status_updated ON drive_file(folderId, status, updatedAt);', | ||
| 'CREATE INDEX IF NOT EXISTS idx_drive_file_status_updated ON drive_file(status, updatedAt);', | ||
| 'CREATE INDEX IF NOT EXISTS idx_drive_folder_parent_status_updated ON drive_folder(parentId, status, updatedAt);', | ||
| 'CREATE INDEX IF NOT EXISTS idx_drive_folder_status_updated ON drive_folder(status, updatedAt);', | ||
| ]; | ||
|
|
||
| export async function initializeVirtualDriveSqlite({ dataSource = AppDataSource }: Props = {}) { | ||
| if (!dataSource.isInitialized) { | ||
| return; | ||
| } | ||
|
|
||
| for (const statement of SQLITE_BOOTSTRAP_STATEMENTS) { | ||
| await dataSource.query(statement); | ||
|
Check warning on line 39 in src/apps/main/database/initialize-virtual-drive-sqlite.ts
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,8 @@ | ||
| import { getUser } from './get-user'; | ||
|
|
||
|
|
||
| export function getUserOrThrow() { | ||
| const { data: user, error } = getUser(); | ||
| if (error) throw error; | ||
| const { data: user, error } = getUser(); | ||
| if (error) throw error; | ||
|
|
||
| return user; | ||
| } | ||
| return user; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const DIRECTORY_STATE_TTL_MS = 30_000; |
64 changes: 64 additions & 0 deletions
64
src/backend/features/virtual-drive/services/lazy/directory-state-sqlite-repository.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { call } from 'tests/vitest/utils.helper'; | ||
| import { DirectoryStateRepository } from './directory-state-sqlite-repository'; | ||
|
|
||
| describe('directory-state-sqlite-repository', () => { | ||
| const queryMock = | ||
| vi.fn<(statement: string, parameters?: Array<unknown>) => Promise<Array<{ children_loaded_at: string | null }>>>(); | ||
| const stateProps = { folderId: 42, statusScope: 'EXISTS' as const }; | ||
| const dataSource = { query: queryMock }; | ||
|
|
||
| beforeEach(() => { | ||
| queryMock.mockResolvedValue([]); | ||
| }); | ||
|
|
||
| it('should report a directory as stale when it has not been loaded', async () => { | ||
| const isFresh = await DirectoryStateRepository.isFresh({ dataSource, ...stateProps }); | ||
|
|
||
| expect(isFresh).toBe(false); | ||
| call(queryMock).toStrictEqual([ | ||
| 'SELECT children_loaded_at FROM drive_directory_state WHERE folder_id = ? AND status_scope = ? LIMIT 1', | ||
| [42, 'EXISTS'], | ||
| ]); | ||
| }); | ||
|
|
||
| it('should report a directory as fresh when its state is within the ttl', async () => { | ||
| queryMock.mockResolvedValue([{ children_loaded_at: new Date().toISOString() }]); | ||
|
|
||
| const isFresh = await DirectoryStateRepository.isFresh({ dataSource, ...stateProps, ttlMs: 30_000 }); | ||
|
|
||
| expect(isFresh).toBe(true); | ||
| }); | ||
|
|
||
| it('should report a directory as stale when its state exceeds the ttl', async () => { | ||
| queryMock.mockResolvedValue([{ children_loaded_at: new Date(Date.now() - 30_001).toISOString() }]); | ||
|
|
||
| const isFresh = await DirectoryStateRepository.isFresh({ dataSource, ...stateProps, ttlMs: 30_000 }); | ||
|
|
||
| expect(isFresh).toBe(false); | ||
| }); | ||
|
|
||
| it('should record a successful directory load', async () => { | ||
| await DirectoryStateRepository.markLoaded({ dataSource, ...stateProps }); | ||
|
|
||
| expect(queryMock).toHaveBeenCalledWith( | ||
| expect.stringContaining('DO UPDATE SET children_loaded_at = excluded.children_loaded_at'), | ||
| [42, 'EXISTS', expect.any(String)], | ||
| ); | ||
| }); | ||
|
|
||
| it('should record a directory load error', async () => { | ||
| await DirectoryStateRepository.markError({ dataSource, ...stateProps }); | ||
|
|
||
| expect(queryMock).toHaveBeenCalledWith(expect.stringContaining('VALUES (?, ?, NULL, ?,'), [ | ||
| 42, | ||
| 'EXISTS', | ||
| expect.any(String), | ||
| ]); | ||
| }); | ||
|
|
||
| it('should clear all directory states', async () => { | ||
| await DirectoryStateRepository.clear({ dataSource }); | ||
|
|
||
| call(queryMock).toStrictEqual('DELETE FROM drive_directory_state'); | ||
| }); | ||
| }); |
57 changes: 57 additions & 0 deletions
57
src/backend/features/virtual-drive/services/lazy/directory-state-sqlite-repository.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { AppDataSource } from '../../../../../apps/main/database/data-source'; | ||
| import { DIRECTORY_STATE_TTL_MS } from './constants'; | ||
| import type { | ||
| DirectoryStateFreshnessProps, | ||
| DirectoryStateOperationProps, | ||
| DirectoryStateRepositoryProps, | ||
| } from './types'; | ||
|
|
||
| async function isFresh({ | ||
| dataSource = AppDataSource, | ||
| folderId, | ||
| statusScope, | ||
| ttlMs = DIRECTORY_STATE_TTL_MS, | ||
| }: DirectoryStateFreshnessProps) { | ||
| const rows = await dataSource.query( | ||
| 'SELECT children_loaded_at FROM drive_directory_state WHERE folder_id = ? AND status_scope = ? LIMIT 1', | ||
| [folderId, statusScope], | ||
| ); | ||
| const loadedAt = rows[0]?.children_loaded_at; | ||
|
|
||
| if (!loadedAt) { | ||
| return false; | ||
| } | ||
|
|
||
| return Date.now() - new Date(loadedAt).getTime() < ttlMs; | ||
| } | ||
|
|
||
| async function markLoaded({ dataSource = AppDataSource, folderId, statusScope }: DirectoryStateOperationProps) { | ||
| await dataSource.query( | ||
| `INSERT INTO drive_directory_state (folder_id, status_scope, children_loaded_at, children_last_error_at, fetch_state) | ||
| VALUES (?, ?, ?, NULL, 'idle') | ||
| ON CONFLICT(folder_id, status_scope) | ||
| DO UPDATE SET children_loaded_at = excluded.children_loaded_at, children_last_error_at = NULL, fetch_state = 'idle'`, | ||
| [folderId, statusScope, new Date().toISOString()], | ||
| ); | ||
| } | ||
|
|
||
| async function markError({ dataSource = AppDataSource, folderId, statusScope }: DirectoryStateOperationProps) { | ||
| await dataSource.query( | ||
| `INSERT INTO drive_directory_state (folder_id, status_scope, children_loaded_at, children_last_error_at, fetch_state) | ||
| VALUES (?, ?, NULL, ?, 'error') | ||
| ON CONFLICT(folder_id, status_scope) | ||
| DO UPDATE SET children_last_error_at = excluded.children_last_error_at, fetch_state = 'error'`, | ||
| [folderId, statusScope, new Date().toISOString()], | ||
| ); | ||
| } | ||
|
|
||
| async function clear({ dataSource = AppDataSource }: DirectoryStateRepositoryProps = {}) { | ||
| await dataSource.query('DELETE FROM drive_directory_state'); | ||
| } | ||
|
|
||
| export const DirectoryStateRepository = { | ||
| isFresh, | ||
| markLoaded, | ||
| markError, | ||
| clear, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| export type DirectoryStatusScope = 'EXISTS'; | ||
|
|
||
| export type DirectoryStateRow = { | ||
| children_loaded_at: string | null; | ||
| }; | ||
|
|
||
| export type SqliteQueryExecutor = { | ||
| query: (statement: string, parameters?: Array<unknown>) => Promise<Array<DirectoryStateRow>>; | ||
| }; | ||
|
|
||
| export type DirectoryStateProps = { | ||
| folderId: number; | ||
| statusScope: DirectoryStatusScope; | ||
| }; | ||
|
|
||
| export type DirectoryStateRepositoryProps = { | ||
| dataSource?: SqliteQueryExecutor; | ||
| }; | ||
|
|
||
| export type DirectoryStateOperationProps = DirectoryStateProps & DirectoryStateRepositoryProps; | ||
|
|
||
| export type DirectoryStateFreshnessProps = DirectoryStateOperationProps & { | ||
| ttlMs?: number; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.