From 3919f07bd41c90d1082edd851afa7b4413c479e4 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 14 Apr 2026 20:16:04 -0400 Subject: [PATCH 1/4] Preserve custom db.php drop-ins during SQLite integration updates Studio unconditionally overwrites wp-content/db.php on every site start, import, export, push, and pull. This breaks plugins that extend the SQLite integration with a custom db.php (e.g. markdown-database-integration). Add hasCustomDbDropin() to detect custom drop-ins by checking if db.php defines SQLITE_DB_DROPIN_VERSION but is not the stock auto-generated file. When a custom drop-in is detected, skip overwriting db.php but still update the SQLite mu-plugin (which custom drop-ins depend on). Fixes #3094 --- tools/common/lib/sqlite-integration.ts | 65 ++++++++++++++--- .../lib/tests/sqlite-integration.test.ts | 72 +++++++++++++++++++ 2 files changed, 127 insertions(+), 10 deletions(-) diff --git a/tools/common/lib/sqlite-integration.ts b/tools/common/lib/sqlite-integration.ts index ecf54f7388..acf3352eae 100644 --- a/tools/common/lib/sqlite-integration.ts +++ b/tools/common/lib/sqlite-integration.ts @@ -23,6 +23,41 @@ export abstract class SqliteIntegrationProvider { return hasDbPhp || ! hasWpConfig; } + /** + * Check if the existing db.php is a custom drop-in that should not be overwritten. + * + * A custom drop-in is one that defines SQLITE_DB_DROPIN_VERSION but was not + * generated by Studio. This allows plugins like markdown-database-integration + * to extend the SQLite integration with a custom db.php while still using the + * same SQLite mu-plugin machinery. + * + * We detect this by checking whether the db.php file was auto-generated from + * db.copy (contains the SQLITE_IMPLEMENTATION_FOLDER_PATH placeholder pattern) + * or is a custom file that defines the version constant on its own. + */ + async hasCustomDbDropin( sitePath: string ): Promise< boolean > { + const dbPhpPath = path.join( sitePath, 'wp-content', 'db.php' ); + if ( ! fs.existsSync( dbPhpPath ) ) { + return false; + } + + try { + const content = await fs.promises.readFile( dbPhpPath, 'utf8' ); + + // The stock db.php (generated from db.copy) contains this comment + // identifying it as auto-generated by the SQLite integration plugin. + const isStockDropin = content.includes( 'This file is auto-generated and copied from the sqlite plugin' ); + + // A custom drop-in defines SQLITE_DB_DROPIN_VERSION but is not the + // stock file. It extends or replaces the SQLite integration layer. + const definesVersion = content.includes( 'SQLITE_DB_DROPIN_VERSION' ); + + return definesVersion && ! isStockDropin; + } catch { + return false; + } + } + async getSqliteVersionFromInstallation( sqliteMuPluginPath: string ): Promise< string > { try { const versionFileContent = await fs.promises.readFile( @@ -46,18 +81,28 @@ export abstract class SqliteIntegrationProvider { await fs.promises.mkdir( databasePath, { recursive: true } ); + // Only write db.php if there is no custom drop-in. Custom drop-ins + // extend the SQLite integration (e.g. markdown-database-integration) + // and should not be overwritten. + const hasCustom = await this.hasCustomDbDropin( sitePath ); + if ( ! hasCustom ) { + const sqliteSourcePath = this.getSqlitePluginSourcePath(); + const dbCopyContent = await fs.promises.readFile( + path.join( sqliteSourcePath, 'db.copy' ), + 'utf8' + ); + const sqliteDirname = this.getSqliteDirname(); + const updatedContent = dbCopyContent.replace( + "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", + `realpath( __DIR__ . '/mu-plugins/${ sqliteDirname }' )` + ); + await fs.promises.writeFile( path.join( wpContentPath, 'db.php' ), updatedContent ); + } + + // Always keep the SQLite mu-plugin updated — custom drop-ins depend on + // the mu-plugin's driver classes even when they replace db.php. const sqliteSourcePath = this.getSqlitePluginSourcePath(); - const dbCopyContent = await fs.promises.readFile( - path.join( sqliteSourcePath, 'db.copy' ), - 'utf8' - ); const sqliteDirname = this.getSqliteDirname(); - const updatedContent = dbCopyContent.replace( - "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", - `realpath( __DIR__ . '/mu-plugins/${ sqliteDirname }' )` - ); - await fs.promises.writeFile( path.join( wpContentPath, 'db.php' ), updatedContent ); - const sqliteDestPath = path.join( wpContentPath, 'mu-plugins', sqliteDirname ); await fs.promises.cp( sqliteSourcePath, sqliteDestPath, { recursive: true, diff --git a/tools/common/lib/tests/sqlite-integration.test.ts b/tools/common/lib/tests/sqlite-integration.test.ts index d5c3190aff..00147df229 100644 --- a/tools/common/lib/tests/sqlite-integration.test.ts +++ b/tools/common/lib/tests/sqlite-integration.test.ts @@ -83,6 +83,43 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { } ); } ); + describe( 'hasCustomDbDropin', () => { + it( 'should return false when db.php does not exist', async () => { + const result = await provider.hasCustomDbDropin( MOCK_SITE_PATH ); + expect( result ).toBe( false ); + } ); + + it( 'should return false for stock db.php', async () => { + volFromJSON( { + [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: + " { + volFromJSON( { + [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: + " { + volFromJSON( { + [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: + ' { beforeEach( () => { volFromJSON( { @@ -109,6 +146,41 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { ); } ); + it( 'should not overwrite custom db.php drop-in', async () => { + volFromJSON( { + [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: + "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", + [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: + " { + volFromJSON( { + [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: + "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", + [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: + " { await provider.installSqliteIntegration( MOCK_SITE_PATH ); From 1995062a972595d2bac4c963ab3d058690b1fa8e Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 15 Apr 2026 07:37:40 -0400 Subject: [PATCH 2/4] fix: resolve prettier lint errors in sqlite-integration Apply prettier formatting to sqlite-integration.ts and its test file to fix CI lint failures. --- tools/common/lib/sqlite-integration.ts | 4 +++- tools/common/lib/tests/sqlite-integration.test.ts | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/common/lib/sqlite-integration.ts b/tools/common/lib/sqlite-integration.ts index acf3352eae..8cb27a968f 100644 --- a/tools/common/lib/sqlite-integration.ts +++ b/tools/common/lib/sqlite-integration.ts @@ -46,7 +46,9 @@ export abstract class SqliteIntegrationProvider { // The stock db.php (generated from db.copy) contains this comment // identifying it as auto-generated by the SQLite integration plugin. - const isStockDropin = content.includes( 'This file is auto-generated and copied from the sqlite plugin' ); + const isStockDropin = content.includes( + 'This file is auto-generated and copied from the sqlite plugin' + ); // A custom drop-in defines SQLITE_DB_DROPIN_VERSION but is not the // stock file. It extends or replaces the SQLite integration layer. diff --git a/tools/common/lib/tests/sqlite-integration.test.ts b/tools/common/lib/tests/sqlite-integration.test.ts index 00147df229..899c1179c0 100644 --- a/tools/common/lib/tests/sqlite-integration.test.ts +++ b/tools/common/lib/tests/sqlite-integration.test.ts @@ -111,8 +111,7 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { it( 'should return false for db.php without SQLITE_DB_DROPIN_VERSION', async () => { volFromJSON( { - [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: - ' Date: Sat, 9 May 2026 15:50:35 -0400 Subject: [PATCH 3/4] test: fix custom db drop-in fixture path --- tools/common/lib/tests/sqlite-integration.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/common/lib/tests/sqlite-integration.test.ts b/tools/common/lib/tests/sqlite-integration.test.ts index 97ddaf2e10..0600a91621 100644 --- a/tools/common/lib/tests/sqlite-integration.test.ts +++ b/tools/common/lib/tests/sqlite-integration.test.ts @@ -147,7 +147,7 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { it( 'should not overwrite custom db.php drop-in', async () => { volFromJSON( { - [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: + [ normalize( `wp-files/${ SQLITE_DIRNAME }/db.copy` ) ]: "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: " { it( 'should still copy mu-plugin when custom db.php exists', async () => { volFromJSON( { - [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: + [ normalize( `wp-files/${ SQLITE_DIRNAME }/db.copy` ) ]: "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: " { // The mu-plugin should still be updated — custom drop-ins depend on it. expect( vi.mocked( fs.promises.cp ) ).toHaveBeenCalledWith( - normalize( `server-files/${ SQLITE_DIRNAME }` ), + normalize( `wp-files/${ SQLITE_DIRNAME }` ), normalize( `${ MOCK_SITE_PATH }/wp-content/mu-plugins/${ SQLITE_DIRNAME }` ), expect.any( Object ) ); From 48bca3844390c3e5e09295a282fe3826789971a6 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 18 May 2026 15:41:54 -0400 Subject: [PATCH 4/4] fix: preserve marked db drop-ins --- tools/common/lib/sqlite-integration.ts | 41 +++---------------- .../lib/tests/sqlite-integration.test.ts | 35 ++++++---------- 2 files changed, 17 insertions(+), 59 deletions(-) diff --git a/tools/common/lib/sqlite-integration.ts b/tools/common/lib/sqlite-integration.ts index 5c1d960b5a..f42a908a6a 100644 --- a/tools/common/lib/sqlite-integration.ts +++ b/tools/common/lib/sqlite-integration.ts @@ -19,19 +19,7 @@ export abstract class SqliteIntegrationProvider { return hasDbPhp || ! hasWpConfig; } - /** - * Check if the existing db.php is a custom drop-in that should not be overwritten. - * - * A custom drop-in is one that defines SQLITE_DB_DROPIN_VERSION but was not - * generated by Studio. This allows plugins like markdown-database-integration - * to extend the SQLite integration with a custom db.php while still using the - * same SQLite mu-plugin machinery. - * - * We detect this by checking whether the db.php file was auto-generated from - * db.copy (contains the SQLITE_IMPLEMENTATION_FOLDER_PATH placeholder pattern) - * or is a custom file that defines the version constant on its own. - */ - async hasCustomDbDropin( sitePath: string ): Promise< boolean > { + async shouldKeepExistingDbDropin( sitePath: string ): Promise< boolean > { const dbPhpPath = path.join( sitePath, 'wp-content', 'db.php' ); if ( ! fs.existsSync( dbPhpPath ) ) { return false; @@ -39,18 +27,7 @@ export abstract class SqliteIntegrationProvider { try { const content = await fs.promises.readFile( dbPhpPath, 'utf8' ); - - // The stock db.php (generated from db.copy) contains this comment - // identifying it as auto-generated by the SQLite integration plugin. - const isStockDropin = content.includes( - 'This file is auto-generated and copied from the sqlite plugin' - ); - - // A custom drop-in defines SQLITE_DB_DROPIN_VERSION but is not the - // stock file. It extends or replaces the SQLite integration layer. - const definesVersion = content.includes( 'SQLITE_DB_DROPIN_VERSION' ); - - return definesVersion && ! isStockDropin; + return content.includes( '@studio-keep' ); } catch { return false; } @@ -76,20 +53,16 @@ export abstract class SqliteIntegrationProvider { const wpContentPath = path.join( sitePath, 'wp-content' ); const databasePath = path.join( wpContentPath, 'database' ); + const sqliteSourcePath = this.getSqlitePluginSourcePath(); + const sqliteDirname = this.getSqliteDirname(); await fs.promises.mkdir( databasePath, { recursive: true } ); - // Only write db.php if there is no custom drop-in. Custom drop-ins - // extend the SQLite integration (e.g. markdown-database-integration) - // and should not be overwritten. - const hasCustom = await this.hasCustomDbDropin( sitePath ); - if ( ! hasCustom ) { - const sqliteSourcePath = this.getSqlitePluginSourcePath(); + if ( ! ( await this.shouldKeepExistingDbDropin( sitePath ) ) ) { const dbCopyContent = await fs.promises.readFile( path.join( sqliteSourcePath, 'db.copy' ), 'utf8' ); - const sqliteDirname = this.getSqliteDirname(); const updatedContent = dbCopyContent.replace( "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", `realpath( __DIR__ . '/mu-plugins/${ sqliteDirname }' )` @@ -97,10 +70,6 @@ export abstract class SqliteIntegrationProvider { await fs.promises.writeFile( path.join( wpContentPath, 'db.php' ), updatedContent ); } - // Always keep the SQLite mu-plugin updated — custom drop-ins depend on - // the mu-plugin's driver classes even when they replace db.php. - const sqliteSourcePath = this.getSqlitePluginSourcePath(); - const sqliteDirname = this.getSqliteDirname(); const sqliteDestPath = path.join( wpContentPath, 'mu-plugins', sqliteDirname ); await fs.promises.rm( sqliteDestPath, { recursive: true, force: true } ); await fs.promises.cp( sqliteSourcePath, sqliteDestPath, { diff --git a/tools/common/lib/tests/sqlite-integration.test.ts b/tools/common/lib/tests/sqlite-integration.test.ts index 0600a91621..d564be814d 100644 --- a/tools/common/lib/tests/sqlite-integration.test.ts +++ b/tools/common/lib/tests/sqlite-integration.test.ts @@ -83,40 +83,31 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { } ); } ); - describe( 'hasCustomDbDropin', () => { + describe( 'shouldKeepExistingDbDropin', () => { it( 'should return false when db.php does not exist', async () => { - const result = await provider.hasCustomDbDropin( MOCK_SITE_PATH ); + const result = await provider.shouldKeepExistingDbDropin( MOCK_SITE_PATH ); expect( result ).toBe( false ); } ); - it( 'should return false for stock db.php', async () => { + it( 'should return false when db.php does not include the Studio keep marker', async () => { volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: - " { + it( 'should return true when db.php includes the Studio keep marker', async () => { volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: - " { - volFromJSON( { - [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: ' { @@ -145,34 +136,32 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { ); } ); - it( 'should not overwrite custom db.php drop-in', async () => { + it( 'should not overwrite db.php drop-in with the Studio keep marker', async () => { volFromJSON( { [ normalize( `wp-files/${ SQLITE_DIRNAME }/db.copy` ) ]: "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: - " { + it( 'should still copy mu-plugin when db.php has the Studio keep marker', async () => { volFromJSON( { [ normalize( `wp-files/${ SQLITE_DIRNAME }/db.copy` ) ]: "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: - "