From f5d674c3e9f19e93044ed67b95745bac652cc170 Mon Sep 17 00:00:00 2001 From: George Garside Date: Wed, 3 Jun 2026 21:07:15 +0100 Subject: [PATCH 1/3] Overwrite db.php before importing database Ensure SQLite integration is in place since importing database reads file to determine version. --- apps/cli/lib/import-export/import/importers/importer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/cli/lib/import-export/import/importers/importer.ts b/apps/cli/lib/import-export/import/importers/importer.ts index 16b8e2d94b..0812b29f6b 100644 --- a/apps/cli/lib/import-export/import/importers/importer.ts +++ b/apps/cli/lib/import-export/import/importers/importer.ts @@ -17,6 +17,7 @@ import semver from 'semver'; import trash from 'trash'; import { SiteData } from 'cli/lib/cli-config/core'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; +import { installSqliteIntegration } from 'cli/lib/sqlite-integration'; import { ImportExportEventEmitter } from '../../events'; import { BackupContents, MetaFileData } from '../types'; import { updateSiteUrl } from '../update-site-url'; @@ -77,6 +78,9 @@ abstract class BaseImporter extends ImportExportEventEmitter implements Importer let processedFiles = 0; const totalFiles = sortedSqlFiles.length; + // db.php may be non-SQLite drop-in from backup + await installSqliteIntegration( site.path ); + for ( const sqlFile of sortedSqlFiles ) { const sqlTempFile = `${ generateBackupFilename( 'sql' ) }.sql`; const tmpPath = path.join( site.path, sqlTempFile ); From 6b352969dc5681e9bd8e27f16ad2f2810d862758 Mon Sep 17 00:00:00 2001 From: George Garside Date: Wed, 15 Jul 2026 22:18:50 +0100 Subject: [PATCH 2/3] Add regression test for foreign db.php in Jetpack backup import --- apps/cli/commands/tests/import.e2e.test.ts | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/apps/cli/commands/tests/import.e2e.test.ts b/apps/cli/commands/tests/import.e2e.test.ts index c808546820..cc7b8c2ed3 100644 --- a/apps/cli/commands/tests/import.e2e.test.ts +++ b/apps/cli/commands/tests/import.e2e.test.ts @@ -17,6 +17,7 @@ */ import fs from 'fs'; import path from 'path'; +import * as tar from 'tar'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { cleanupCliEnv, @@ -187,4 +188,45 @@ describe.skipIf( ! cliE2ePrerequisitesMet() )( 'CLI e2e: studio import', () => { expect( await getBlogname( env, sitePath ) ).toBe( FIXTURE_BLOGNAME ); } ); + + // Regression test for https://github.com/Automattic/studio/issues/3518 + // where db.php overwrites SQLite drop-in during restore causing database import error + // "Could not determine the version of the SQLite integration plugin". + it( + 'imports a backup containing db.php drop-in', + { tags: [ 'e2e' ], timeout: 300_000 }, + async () => { + if ( ! env ) { + throw new Error( 'CLI e2e env was not initialised' ); + } + + const workDir = path.join( env.root, 'db-php' ); + const cwd = path.join( workDir, 'contents' ); + fs.mkdirSync( cwd, { recursive: true } ); + await tar.x( { file: path.join( FIXTURES_DIR, 'jetpack-backup.tar.gz' ), cwd } ); + + fs.writeFileSync( + path.join( cwd, 'wp-content', 'db.php' ), + ' Date: Mon, 27 Jul 2026 08:21:34 -0400 Subject: [PATCH 3/3] Preserve MySQL classification during database import --- apps/cli/commands/tests/import.e2e.test.ts | 42 +++++++++++++++++++ .../import/importers/importer.ts | 17 +++++++- apps/cli/lib/sqlite-integration.ts | 4 ++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/apps/cli/commands/tests/import.e2e.test.ts b/apps/cli/commands/tests/import.e2e.test.ts index cc7b8c2ed3..a1b6d8c184 100644 --- a/apps/cli/commands/tests/import.e2e.test.ts +++ b/apps/cli/commands/tests/import.e2e.test.ts @@ -229,4 +229,46 @@ describe.skipIf( ! cliE2ePrerequisitesMet() )( 'CLI e2e: studio import', () => { expect( await getBlogname( env, sitePath ) ).toBe( FIXTURE_BLOGNAME ); } ); + + it( + 'rejects a database backup before mutating a site configured for MySQL', + { tags: [ 'e2e' ], timeout: 300_000 }, + async () => { + if ( ! env ) { + throw new Error( 'CLI e2e env was not initialised' ); + } + + const sitePath = await createStoppedSite( env, 'MySQL Import E2E Site', 'import-mysql' ); + const sqlitePaths = [ + path.join( sitePath, 'wp-content', 'db.php' ), + path.join( sitePath, 'wp-content', 'database', '.ht.sqlite' ), + path.join( sitePath, 'wp-content', 'mu-plugins', 'sqlite-database-integration' ), + ]; + fs.copyFileSync( + path.join( sitePath, 'wp-config-sample.php' ), + path.join( sitePath, 'wp-config.php' ) + ); + + for ( const sqlitePath of sqlitePaths ) { + fs.rmSync( sqlitePath, { recursive: true, force: true } ); + } + expect( fs.existsSync( path.join( sitePath, 'wp-config.php' ) ) ).toBe( true ); + + const result = await runCli( + [ 'import', path.join( FIXTURES_DIR, 'jetpack-backup.tar.gz' ), '--path', sitePath ], + env + ); + expect( result.code ).not.toBe( 0 ); + expect( result.stderr ).toContain( + 'Database import requires SQLite, but this site is configured to use an external database.' + ); + + for ( const sqlitePath of sqlitePaths ) { + expect( fs.existsSync( sqlitePath ) ).toBe( false ); + } + expect( fs.existsSync( path.join( sitePath, 'wp-content', 'themes', 'mypet-theme' ) ) ).toBe( + false + ); + } + ); } ); diff --git a/apps/cli/lib/import-export/import/importers/importer.ts b/apps/cli/lib/import-export/import/importers/importer.ts index 0812b29f6b..47f3758857 100644 --- a/apps/cli/lib/import-export/import/importers/importer.ts +++ b/apps/cli/lib/import-export/import/importers/importer.ts @@ -17,7 +17,7 @@ import semver from 'semver'; import trash from 'trash'; import { SiteData } from 'cli/lib/cli-config/core'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; -import { installSqliteIntegration } from 'cli/lib/sqlite-integration'; +import { installSqliteIntegration, needsSqliteSetup } from 'cli/lib/sqlite-integration'; import { ImportExportEventEmitter } from '../../events'; import { BackupContents, MetaFileData } from '../types'; import { updateSiteUrl } from '../update-site-url'; @@ -67,11 +67,22 @@ abstract class BaseImporter extends ImportExportEventEmitter implements Importer abstract import( site: SiteData ): Promise< ImporterResult >; + protected async assertSqliteDatabaseImportSupported( site: SiteData ): Promise< void > { + if ( ! ( await needsSqliteSetup( site.path ) ) ) { + throw new Error( + __( + 'Database import requires SQLite, but this site is configured to use an external database.' + ) + ); + } + } + protected async importDatabase( site: SiteData, sqlFiles: string[] ): Promise< void > { if ( ! sqlFiles.length ) { return; } + await this.assertSqliteDatabaseImportSupported( site ); this.emit( ImportEvents.IMPORT_DATABASE_START ); const sortedSqlFiles = sqlFiles.sort( ( a, b ) => a.localeCompare( b ) ); @@ -149,6 +160,10 @@ abstract class BaseBackupImporter extends BaseImporter { async import( site: SiteData ): Promise< ImporterResult > { try { + if ( this.backup.sqlFiles.length ) { + // Classify the destination before restored files can make MySQL look like SQLite. + await this.assertSqliteDatabaseImportSupported( site ); + } if ( this.shouldCleanUpBeforeImport ) { await this.moveExistingWpContentToTrash( site.path ); } diff --git a/apps/cli/lib/sqlite-integration.ts b/apps/cli/lib/sqlite-integration.ts index a5a5ee3151..a5e57a732d 100644 --- a/apps/cli/lib/sqlite-integration.ts +++ b/apps/cli/lib/sqlite-integration.ts @@ -24,6 +24,10 @@ export async function installSqliteIntegration( sitePath: string ) { return provider.installSqliteIntegration( sitePath ); } +export async function needsSqliteSetup( sitePath: string ) { + return provider.needsSqliteSetup( sitePath ); +} + export async function keepSqliteIntegrationUpdated( sitePath: string ) { return provider.keepSqliteIntegrationUpdated( sitePath ); }