From fb54de0052e79afd7988e8130c7e4663ef6fe871 Mon Sep 17 00:00:00 2001 From: Makito Date: Sat, 22 Feb 2025 20:17:37 +0800 Subject: [PATCH] fix: respect access mode and file open flags --- .../duckdb-wasm/src/bindings/bindings_base.ts | 10 +- .../src/bindings/bindings_interface.ts | 6 +- packages/duckdb-wasm/src/bindings/runtime.ts | 15 ++- .../src/bindings/runtime_browser.ts | 23 ++-- .../duckdb-wasm/src/bindings/runtime_node.ts | 32 ++++-- .../src/parallel/worker_dispatcher.ts | 3 +- packages/duckdb-wasm/test/index_node.ts | 2 + packages/duckdb-wasm/test/nodefs.test.ts | 71 ++++++++++++ packages/duckdb-wasm/test/opfs.test.ts | 108 +++++++++++++++--- patches/duckdb/fix_load_database.patch | 31 +++-- 10 files changed, 249 insertions(+), 52 deletions(-) create mode 100644 packages/duckdb-wasm/test/nodefs.test.ts diff --git a/packages/duckdb-wasm/src/bindings/bindings_base.ts b/packages/duckdb-wasm/src/bindings/bindings_base.ts index f395bdb10..24f0cd615 100644 --- a/packages/duckdb-wasm/src/bindings/bindings_base.ts +++ b/packages/duckdb-wasm/src/bindings/bindings_base.ts @@ -1,5 +1,5 @@ import { DuckDBModule, PThread } from './duckdb_module'; -import { DuckDBConfig } from './config'; +import { DuckDBAccessMode, DuckDBConfig } from './config'; import { Logger } from '../log'; import { InstantiationProgress } from './progress'; import { DuckDBBindings } from './bindings_interface'; @@ -469,9 +469,9 @@ export abstract class DuckDBBindingsBase implements DuckDBBindings { } dropResponseBuffers(this.mod); } - public async prepareFileHandle(fileName: string, protocol: DuckDBDataProtocol): Promise { + public async prepareFileHandle(fileName: string, protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode): Promise { if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS && this._runtime.prepareFileHandles) { - const list = await this._runtime.prepareFileHandles([fileName], DuckDBDataProtocol.BROWSER_FSACCESS); + const list = await this._runtime.prepareFileHandles([fileName], DuckDBDataProtocol.BROWSER_FSACCESS, accessMode); for (const item of list) { const { handle, path: filePath, fromCached } = item; if (!fromCached && handle.getSize()) { @@ -483,9 +483,9 @@ export abstract class DuckDBBindingsBase implements DuckDBBindings { throw new Error(`prepareFileHandle: unsupported protocol ${protocol}`); } /** Prepare a file handle that could only be acquired aschronously */ - public async prepareDBFileHandle(path: string, protocol: DuckDBDataProtocol): Promise { + public async prepareDBFileHandle(path: string, protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode): Promise { if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS && this._runtime.prepareDBFileHandle) { - const list = await this._runtime.prepareDBFileHandle(path, DuckDBDataProtocol.BROWSER_FSACCESS); + const list = await this._runtime.prepareDBFileHandle(path, DuckDBDataProtocol.BROWSER_FSACCESS, accessMode); for (const item of list) { const { handle, path: filePath, fromCached } = item; if (!fromCached && handle.getSize()) { diff --git a/packages/duckdb-wasm/src/bindings/bindings_interface.ts b/packages/duckdb-wasm/src/bindings/bindings_interface.ts index 271a42ef9..3a5e3049d 100644 --- a/packages/duckdb-wasm/src/bindings/bindings_interface.ts +++ b/packages/duckdb-wasm/src/bindings/bindings_interface.ts @@ -1,4 +1,4 @@ -import { DuckDBConfig, DuckDBConnection, DuckDBDataProtocol, FileStatistics, InstantiationProgress } from '.'; +import { DuckDBAccessMode, DuckDBConfig, DuckDBConnection, DuckDBDataProtocol, FileStatistics, InstantiationProgress } from '.'; import { CSVInsertOptions, JSONInsertOptions, ArrowInsertOptions } from './insert_options'; import { ScriptTokens } from './tokens'; import { WebFile } from './web_file'; @@ -54,8 +54,8 @@ export interface DuckDBBindings { protocol: DuckDBDataProtocol, directIO: boolean, ): Promise; - prepareFileHandle(path: string, protocol: DuckDBDataProtocol): Promise; - prepareDBFileHandle(path: string, protocol: DuckDBDataProtocol): Promise; + prepareFileHandle(path: string, protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode): Promise; + prepareDBFileHandle(path: string, protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode): Promise; globFiles(path: string): WebFile[]; dropFile(name: string): void; dropFiles(): void; diff --git a/packages/duckdb-wasm/src/bindings/runtime.ts b/packages/duckdb-wasm/src/bindings/runtime.ts index b720e6877..c611ca4d9 100644 --- a/packages/duckdb-wasm/src/bindings/runtime.ts +++ b/packages/duckdb-wasm/src/bindings/runtime.ts @@ -1,3 +1,4 @@ +import { DuckDBAccessMode } from './config'; import { DuckDBModule } from './duckdb_module'; import { UDFFunction } from './udf_function'; import * as udf_rt from './udf_runtime'; @@ -58,6 +59,16 @@ export enum FileFlags { FILE_FLAGS_FILE_CREATE_NEW = 1 << 4, //! Open file in append mode FILE_FLAGS_APPEND = 1 << 5, + //! Open file with restrictive permissions (600 on linux/mac) can only be used when creating, throws if file exists + FILE_FLAGS_PRIVATE = 1 << 6, + //! Return NULL if the file does not exist instead of throwing an error + FILE_FLAGS_NULL_IF_NOT_EXISTS = 1 << 7, + //! Multiple threads may perform reads and writes in parallel + FILE_FLAGS_PARALLEL_ACCESS = 1 << 8, + //! Ensure that this call creates the file, throw is file exists + FILE_FLAGS_EXCLUSIVE_CREATE = 1 << 9, + //! Return NULL if the file exist instead of throwing an error + FILE_FLAGS_NULL_IF_EXISTS = 1 << 10, } /** Configuration for the AWS S3 Filesystem */ @@ -158,8 +169,8 @@ export interface DuckDBRuntime { // Prepare a file handle that could only be acquired aschronously prepareFileHandle?: (path: string, protocol: DuckDBDataProtocol) => Promise; - prepareFileHandles?: (path: string[], protocol: DuckDBDataProtocol) => Promise; - prepareDBFileHandle?: (path: string, protocol: DuckDBDataProtocol) => Promise; + prepareFileHandles?: (path: string[], protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode) => Promise; + prepareDBFileHandle?: (path: string, protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode) => Promise; // Call a scalar UDF function callScalarUDF( diff --git a/packages/duckdb-wasm/src/bindings/runtime_browser.ts b/packages/duckdb-wasm/src/bindings/runtime_browser.ts index 0b4ebedca..cd187f64f 100644 --- a/packages/duckdb-wasm/src/bindings/runtime_browser.ts +++ b/packages/duckdb-wasm/src/bindings/runtime_browser.ts @@ -15,6 +15,7 @@ import { } from './runtime'; import { DuckDBModule } from './duckdb_module'; import * as udf from './udf_runtime'; +import { DuckDBAccessMode } from './config'; const OPFS_PREFIX_LEN = 'opfs://'.length; const PATH_SEP_REGEX = /\/|\\/; @@ -110,8 +111,11 @@ export const BROWSER_RUNTIME: DuckDBRuntime & { BROWSER_RUNTIME._opfsRoot = await navigator.storage.getDirectory(); } }, - /** Prepare a file handle that could only be acquired aschronously */ - async prepareFileHandles(filePaths: string[], protocol: DuckDBDataProtocol): Promise { + /** Prepare a file handle that could only be acquired asynchronously */ + async prepareFileHandles(filePaths: string[], protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode): Promise { + // DuckDBAccessMode.UNDEFINED will be treated as READ_WRITE + // See: https://github.com/duckdb/duckdb/blob/5f5512b827df6397afd31daedb4bbdee76520019/src/main/database.cpp#L442-L444 + const isReadWrite = !accessMode || accessMode === DuckDBAccessMode.READ_WRITE; if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS) { await BROWSER_RUNTIME.assignOPFSRoot(); const prepare = async (path: string): Promise => { @@ -135,13 +139,16 @@ export const BROWSER_RUNTIME: DuckDBRuntime & { } // mkdir -p for (const folder of folders) { - dirHandle = await dirHandle.getDirectoryHandle(folder, { create: true }); + dirHandle = await dirHandle.getDirectoryHandle(folder, { create: isReadWrite }); } } const fileHandle = await dirHandle.getFileHandle(fileName, { create: false }).catch(e => { if (e?.name === 'NotFoundError') { - console.debug(`File ${path} does not exists yet, creating...`); - return dirHandle.getFileHandle(fileName, { create: true }); + if (isReadWrite) { + console.debug(`File ${path} does not exists yet, creating...`); + return dirHandle.getFileHandle(fileName, { create: true }); + } + console.debug(`File ${path} does not exists, aborting as we are in read-only mode`); } throw e; }); @@ -166,11 +173,11 @@ export const BROWSER_RUNTIME: DuckDBRuntime & { } throw new Error(`Unsupported protocol ${protocol} for paths ${filePaths} with protocol ${protocol}`); }, - /** Prepare a file handle that could only be acquired aschronously */ - async prepareDBFileHandle(dbPath: string, protocol: DuckDBDataProtocol): Promise { + /** Prepare a file handle that could only be acquired asynchronously */ + async prepareDBFileHandle(dbPath: string, protocol: DuckDBDataProtocol, accessMode?: DuckDBAccessMode): Promise { if (protocol === DuckDBDataProtocol.BROWSER_FSACCESS && this.prepareFileHandles) { const filePaths = [dbPath, `${dbPath}.wal`]; - return this.prepareFileHandles(filePaths, protocol); + return this.prepareFileHandles(filePaths, protocol, accessMode); } throw new Error(`Unsupported protocol ${protocol} for path ${dbPath} with protocol ${protocol}`); }, diff --git a/packages/duckdb-wasm/src/bindings/runtime_node.ts b/packages/duckdb-wasm/src/bindings/runtime_node.ts index 2f92368bc..38cdb62af 100644 --- a/packages/duckdb-wasm/src/bindings/runtime_node.ts +++ b/packages/duckdb-wasm/src/bindings/runtime_node.ts @@ -74,16 +74,32 @@ export const NODE_RUNTIME: DuckDBRuntime & { switch (file?.dataProtocol) { // Native file case DuckDBDataProtocol.NODE_FS: { + let openFlags = fs.constants.O_RDONLY; + if (flags & FileFlags.FILE_FLAGS_WRITE) { + openFlags = fs.constants.O_RDWR; + } + if (flags & FileFlags.FILE_FLAGS_FILE_CREATE) { + openFlags |= fs.constants.O_CREAT; + } else if (flags & FileFlags.FILE_FLAGS_FILE_CREATE_NEW) { + openFlags |= fs.constants.O_TRUNC; + } let fd = NODE_RUNTIME._files?.get(file.dataUrl!); - if (fd === null || fd === undefined) { - fd = fs.openSync( - file.dataUrl!, - fs.constants.O_CREAT | fs.constants.O_RDWR, - fs.constants.S_IRUSR | fs.constants.S_IWUSR, - ); - NODE_RUNTIME._filesById?.set(file.fileId!, fd); + let fileSize = 0; + try { + if (fd === null || fd === undefined) { + fd = fs.openSync(file.dataUrl!, openFlags, fs.constants.S_IRUSR | fs.constants.S_IWUSR); + NODE_RUNTIME._filesById?.set(file.fileId!, fd); + } + fileSize = fs.fstatSync(fd).size; + } + catch (e: any) { + if (e.code === 'ENOENT' && (flags & FileFlags.FILE_FLAGS_NULL_IF_NOT_EXISTS)) { + // No-op because we intend to ignore ENOENT while the file does not exist + return 0; // nullptr + } else { + throw e; + } } - const fileSize = fs.fstatSync(fd).size; const result = mod._malloc(2 * 8); mod.HEAPF64[(result >> 3) + 0] = +fileSize; mod.HEAPF64[(result >> 3) + 1] = 0; diff --git a/packages/duckdb-wasm/src/parallel/worker_dispatcher.ts b/packages/duckdb-wasm/src/parallel/worker_dispatcher.ts index 3a5a8f295..402b83e45 100644 --- a/packages/duckdb-wasm/src/parallel/worker_dispatcher.ts +++ b/packages/duckdb-wasm/src/parallel/worker_dispatcher.ts @@ -136,8 +136,9 @@ export abstract class AsyncDuckDBDispatcher implements Logger { case WorkerRequestType.OPEN: { const path = request.data.path; + const accessMode = request.data.accessMode; if (path?.startsWith('opfs://')) { - await this._bindings.prepareDBFileHandle(path, DuckDBDataProtocol.BROWSER_FSACCESS); + await this._bindings.prepareDBFileHandle(path, DuckDBDataProtocol.BROWSER_FSACCESS, accessMode); request.data.useDirectIO = true; } this._bindings.open(request.data); diff --git a/packages/duckdb-wasm/test/index_node.ts b/packages/duckdb-wasm/test/index_node.ts index d170ac88f..d8404b603 100644 --- a/packages/duckdb-wasm/test/index_node.ts +++ b/packages/duckdb-wasm/test/index_node.ts @@ -69,6 +69,7 @@ import { testAllTypes, testAllTypesAsync } from './all_types.test'; import { testBindings, testAsyncBindings } from './bindings.test'; import { testBatchStream } from './batch_stream.test'; import { testFilesystem } from './filesystem.test'; +import { testNodeFS } from './nodefs.test'; import { testAsyncBatchStream } from './batch_stream_async.test'; import { testArrowInsert, testArrowInsertAsync } from './insert_arrow.test'; import { testJSONInsert, testJSONInsertAsync } from './insert_json.test'; @@ -92,6 +93,7 @@ testAsyncBindings(() => adb!, dataDir, duckdb.DuckDBDataProtocol.NODE_FS); testBatchStream(() => db!); testAsyncBatchStream(() => adb!); testFilesystem(() => adb!, resolveData, dataDir, duckdb.DuckDBDataProtocol.NODE_FS); +testNodeFS(() => adb!); testArrowInsert(() => db!); testArrowInsertAsync(() => adb!); testJSONInsert(() => db!); diff --git a/packages/duckdb-wasm/test/nodefs.test.ts b/packages/duckdb-wasm/test/nodefs.test.ts new file mode 100644 index 000000000..152c841b2 --- /dev/null +++ b/packages/duckdb-wasm/test/nodefs.test.ts @@ -0,0 +1,71 @@ +import * as duckdb from '../src/'; +import { tmpdir } from 'os'; +import { randomUUID } from 'crypto'; +import path from 'path'; +import { unlink } from 'fs/promises'; + +export function testNodeFS(db: () => duckdb.AsyncDuckDB): void { + const files: string[] = []; + + afterAll(async () => { + await Promise.all(files.map(file => unlink(file).catch(() => {}))); + await db().flushFiles(); + await db().dropFiles(); + }); + + describe('Node FS', () => { + it('Should not create an empty DB file in read-only mode for non-existent path', async () => { + const tmp = tmpdir(); + const filename = `duckdb_test_${randomUUID().replace(/-/g, '')}`; + files.push(path.join(tmp, filename)); + + await expectAsync( + db().open({ + path: path.join(tmp, filename), + accessMode: duckdb.DuckDBAccessMode.READ_ONLY, + }), + ).toBeRejectedWithError(/database does not exist/); + }); + + it('Should create DB file in read-write mode for non-existent path', async () => { + const tmp = tmpdir(); + const filename = `duckdb_test_${randomUUID().replace(/-/g, '')}`; + files.push(path.join(tmp, filename)); + + await expectAsync( + db().open({ + path: path.join(tmp, filename), + accessMode: duckdb.DuckDBAccessMode.READ_WRITE, + }), + ).toBeResolved(); + }); + + it('Should create an empty DB file in read-only mode for non-existent path with direct I/O', async () => { + const tmp = tmpdir(); + const filename = `duckdb_test_${randomUUID().replace(/-/g, '')}`; + files.push(path.join(tmp, filename)); + + await expectAsync( + db().open({ + path: path.join(tmp, filename), + accessMode: duckdb.DuckDBAccessMode.READ_ONLY, + useDirectIO: true, + }), + ).toBeRejectedWithError(/database does not exist/); + }); + + it('Should create DB file in read-write mode for non-existent path with direct I/O', async () => { + const tmp = tmpdir(); + const filename = `duckdb_test_${randomUUID().replace(/-/g, '')}`; + files.push(path.join(tmp, filename)); + + await expectAsync( + db().open({ + path: path.join(tmp, filename), + accessMode: duckdb.DuckDBAccessMode.READ_WRITE, + useDirectIO: true, + }), + ).toBeResolved(); + }); + }); +} diff --git a/packages/duckdb-wasm/test/opfs.test.ts b/packages/duckdb-wasm/test/opfs.test.ts index eaf1a0fcc..e3087cd26 100644 --- a/packages/duckdb-wasm/test/opfs.test.ts +++ b/packages/duckdb-wasm/test/opfs.test.ts @@ -278,30 +278,106 @@ export function testOPFS(baseDir: string, bundle: () => duckdb.DuckDBBundle): vo }); }); - async function removeFiles() { - const opfsRoot = await navigator.storage.getDirectory(); - await opfsRoot.removeEntry('test.db').catch(() => { - }); - await opfsRoot.removeEntry('test.db.wal').catch(() => { - }); - await opfsRoot.removeEntry('test.csv').catch(() => { - }); - await opfsRoot.removeEntry('test1.csv').catch(() => { + describe('Open database in OPFS', () => { + it('should not open a non-existent DB file in read-only', async () => { + const logger = new duckdb.ConsoleLogger(LogLevel.ERROR); + const worker = new Worker(bundle().mainWorker!); + const db_ = new duckdb.AsyncDuckDB(logger, worker); + await db_.instantiate(bundle().mainModule, bundle().pthreadWorker); + + await expectAsync(db_.open({ + path: 'opfs://non_existent.db', + accessMode: duckdb.DuckDBAccessMode.READ_ONLY, + })).toBeRejectedWithError(Error, /file or directory could not be found/); + + await db_.terminate(); + await worker.terminate(); + + // Files should not be found with DuckDBAccessMode.READ_ONLY + const opfsRoot = await navigator.storage.getDirectory(); + await expectAsync(opfsRoot.getFileHandle('non_existent.db', { create: false })) + .toBeRejectedWithError(Error, /file or directory could not be found/); }); - await opfsRoot.removeEntry('test2.csv').catch(() => { + + it('should not open a non-existent DB file and mkdir in read-only', async () => { + const logger = new duckdb.ConsoleLogger(LogLevel.ERROR); + const worker = new Worker(bundle().mainWorker!); + const db_ = new duckdb.AsyncDuckDB(logger, worker); + await db_.instantiate(bundle().mainModule, bundle().pthreadWorker); + + await expectAsync(db_.open({ + path: 'opfs://duckdb_test/path/to/non_existent.db', + accessMode: duckdb.DuckDBAccessMode.READ_ONLY, + })).toBeRejectedWithError(Error, /file or directory could not be found/); + + await db_.terminate(); + await worker.terminate(); }); - await opfsRoot.removeEntry('test3.csv').catch(() => { + + it('should open a non-existent DB file and mkdir in read-write', async () => { + const logger = new duckdb.ConsoleLogger(LogLevel.ERROR); + const worker = new Worker(bundle().mainWorker!); + const db_ = new duckdb.AsyncDuckDB(logger, worker); + await db_.instantiate(bundle().mainModule, bundle().pthreadWorker); + + await expectAsync(db_.open({ + path: 'opfs://duckdb_test/path/to/duck.db', + accessMode: duckdb.DuckDBAccessMode.READ_WRITE, + })).toBeResolved(); + + await db_.terminate(); + await worker.terminate(); }); - await opfsRoot.removeEntry('test.parquet').catch(() => { + + it('should open a non-existent DB file in read-write and create files', async () => { + const logger = new duckdb.ConsoleLogger(LogLevel.ERROR); + const worker = new Worker(bundle().mainWorker!); + const db_ = new duckdb.AsyncDuckDB(logger, worker); + await db_.instantiate(bundle().mainModule, bundle().pthreadWorker); + + const opfsRoot = await navigator.storage.getDirectory(); + + // Ensure files do not exist + await expectAsync(opfsRoot.getFileHandle('non_existent_2.db', { create: false })) + .toBeRejectedWithError(Error, /file or directory could not be found/); + await expectAsync(opfsRoot.getFileHandle('non_existent_2.db.wal', { create: false })) + .toBeRejectedWithError(Error, /file or directory could not be found/); + + await expectAsync(db_.open({ + path: 'opfs://non_existent_2.db', + accessMode: duckdb.DuckDBAccessMode.READ_WRITE, + })).toBeResolved(); + + await db_.terminate(); + await worker.terminate(); + + // Files should be found with DuckDBAccessMode.READ_WRITE + await expectAsync(opfsRoot.getFileHandle('non_existent_2.db', { create: false })).toBeResolved(); + await expectAsync(opfsRoot.getFileHandle('non_existent_2.db.wal', { create: false })).toBeResolved(); }); + }) + + async function removeFiles() { + const opfsRoot = await navigator.storage.getDirectory(); + await opfsRoot.removeEntry('test.db').catch(() => {}); + await opfsRoot.removeEntry('test.db.wal').catch(() => {}); + await opfsRoot.removeEntry('test.csv').catch(() => {}); + await opfsRoot.removeEntry('test1.csv').catch(() => {}); + await opfsRoot.removeEntry('test2.csv').catch(() => {}); + await opfsRoot.removeEntry('test3.csv').catch(() => {}); + await opfsRoot.removeEntry('test.parquet').catch(() => {}); try { const datadir = await opfsRoot.getDirectoryHandle('datadir'); - datadir.removeEntry('test.parquet').catch(() => { - }); + datadir.removeEntry('test.parquet').catch(() => {}); } catch (e) { // } - await opfsRoot.removeEntry('datadir').catch(() => { - }); + await opfsRoot.removeEntry('datadir').catch(() => {}); + // In case of failure caused leftovers + await opfsRoot.removeEntry('non_existent.db').catch(() => {}); + await opfsRoot.removeEntry('non_existent.db.wal').catch(() => {}); + await opfsRoot.removeEntry('non_existent_2.db').catch(() => {}); + await opfsRoot.removeEntry('non_existent_2.db.wal').catch(() => {}); + await opfsRoot.removeEntry('duckdb_test', { recursive: true }).catch(() => {}); } } diff --git a/patches/duckdb/fix_load_database.patch b/patches/duckdb/fix_load_database.patch index 5399bfc4d..7c5c6b5bc 100644 --- a/patches/duckdb/fix_load_database.patch +++ b/patches/duckdb/fix_load_database.patch @@ -1,20 +1,33 @@ diff --git a/src/storage/storage_manager.cpp b/src/storage/storage_manager.cpp -index cb6c654e5f..a6b2af3b85 100644 +index cb6c654e5f..4a8bb03de0 100644 --- a/src/storage/storage_manager.cpp +++ b/src/storage/storage_manager.cpp -@@ -160,9 +160,12 @@ void SingleFileStorageManager::LoadDatabase(StorageOptions storage_options) { +@@ -160,11 +160,23 @@ void SingleFileStorageManager::LoadDatabase(StorageOptions storage_options) { row_group_size, STANDARD_VECTOR_SIZE); } } - // Check if the database file already exists. - // Note: a file can also exist if there was a ROLLBACK on a previous transaction creating that file. - if (!read_only && !fs.FileExists(path)) { -+ auto db_file_handle = fs.OpenFile(path, FileFlags::FILE_FLAGS_READ | FileFlags::FILE_FLAGS_NULL_IF_NOT_EXISTS); -+ bool is_empty_file = db_file_handle->GetFileSize() == 0; -+ db_file_handle.reset(); +- // file does not exist and we are in read-write mode +- // create a new file + -+ // first check if the database exists -+ if (!read_only && ( !fs.FileExists(path) || ( options.use_direct_io && is_empty_file )) ) { - // file does not exist and we are in read-write mode - // create a new file ++ bool create_or_trunc = false; ++ if (!read_only) { ++ if (fs.FileExists(path)) { ++ auto db_file_handle = fs.OpenFile(path, FileFlags::FILE_FLAGS_READ | FileFlags::FILE_FLAGS_NULL_IF_NOT_EXISTS); ++ create_or_trunc = db_file_handle != nullptr ? db_file_handle->GetFileSize() == 0 : true; ++ if (db_file_handle != nullptr) { ++ db_file_handle.reset(); ++ } ++ } else { ++ create_or_trunc = true; ++ } ++ } ++ ++ if (create_or_trunc) { ++ // We are in read-write mode and the file does not exist or is empty ++ // Create a new database file or truncate the existing one + // check if a WAL file already exists + auto wal_path = GetWALPath();