diff --git a/apps/cli/ai/tests/wp-cli-description.test.ts b/apps/cli/ai/tests/wp-cli-description.test.ts new file mode 100644 index 0000000000..2dfbce40c4 --- /dev/null +++ b/apps/cli/ai/tests/wp-cli-description.test.ts @@ -0,0 +1,17 @@ +import { getSiteDatabaseEngine } from '@studio/common/lib/site-database'; +import { runWpCliTool } from '../tools/wp-cli'; + +vi.mock( '@studio/common/lib/site-database', async ( importActual ) => ( { + ...( await importActual< typeof import('@studio/common/lib/site-database') >() ), + getSiteDatabaseEngine: vi.fn().mockReturnValue( 'sqlite' ), +} ) ); + +describe( 'WP-CLI database description', () => { + it( 'uses the default engine helper and preserves the SQLite guidance verbatim', () => { + expect( getSiteDatabaseEngine ).toHaveBeenCalledWith(); + expect( runWpCliTool.description ).toContain( + 'Studio sites use SQLite, not MySQL (the database file is `/wp-content/database/.ht.sqlite`): `db query` and the other `db` subcommands do not work, so query WordPress data with object commands such as `post list`, `option get`, or `user list`. ' + + 'Users can browse the database themselves with phpMyAdmin from the site overview in the Studio app while the site is running, or with `sqlite3 /wp-content/database/.ht.sqlite` (pre-installed on macOS).' + ); + } ); +} ); diff --git a/apps/cli/ai/tools/wp-cli.ts b/apps/cli/ai/tools/wp-cli.ts index 4df8cab63c..ae30c19214 100644 --- a/apps/cli/ai/tools/wp-cli.ts +++ b/apps/cli/ai/tools/wp-cli.ts @@ -1,3 +1,4 @@ +import { getSiteDatabaseEngine, getSiteDatabasePath } from '@studio/common/lib/site-database'; import { Type } from 'typebox'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; @@ -114,14 +115,24 @@ function getUnsupportedWpCliOptionMessage( args: string[] ): string | null { return `Unsupported WP-CLI option "${ unsupportedOption }": use ASCII hyphens, for example "--porcelain", not a typographic dash.`; } +function getDatabaseDescription(): string { + if ( getSiteDatabaseEngine() !== 'sqlite' ) { + return ''; + } + const databasePath = getSiteDatabasePath( '' ).replace( /\\/g, '/' ); + return ( + `Studio sites use SQLite, not MySQL (the database file is \`${ databasePath }\`): \`db query\` and the other \`db\` subcommands do not work, so query WordPress data with object commands such as \`post list\`, \`option get\`, or \`user list\`. ` + + `Users can browse the database themselves with phpMyAdmin from the site overview in the Studio app while the site is running, or with \`sqlite3 ${ databasePath }\` (pre-installed on macOS).` + ); +} + export const runWpCliTool = defineTool( 'wp_cli', 'Runs a WP-CLI command on a specific WordPress site. The site must be running (site_start if needed). ' + 'Examples: "plugin install woocommerce --activate", "option get blogname", "user list". ' + 'The command takes literal arguments, not shell syntax — never use shell substitution or shell syntax such as `$(cat file)`, backticks, pipes, redirection, or environment variables to provide post content. ' + 'For large post content, write the validated markup to a scratch file inside the site directory and pass its path to `post create ` (or `post update `) — this avoids the OS command-length limit. For smaller content you may instead pass it inline with `--post_content=...` as the final argument. ' + - 'Studio sites use SQLite, not MySQL (the database file is `/wp-content/database/.ht.sqlite`): `db query` and the other `db` subcommands do not work, so query WordPress data with object commands such as `post list`, `option get`, or `user list`. ' + - 'Users can browse the database themselves with phpMyAdmin from the site overview in the Studio app while the site is running, or with `sqlite3 /wp-content/database/.ht.sqlite` (pre-installed on macOS).', + getDatabaseDescription(), { nameOrPath: Type.String( { description: 'The site name or file system path to the site' } ), command: Type.String( { diff --git a/apps/cli/commands/config/set.ts b/apps/cli/commands/config/set.ts index 8d50fcad32..2375618c33 100644 --- a/apps/cli/commands/config/set.ts +++ b/apps/cli/commands/config/set.ts @@ -8,6 +8,7 @@ import { validateAdminEmail, validateAdminUsername, } from '@studio/common/lib/passwords'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { getSiteFileAccess, siteFileAccessSchema, @@ -81,7 +82,7 @@ async function isUnprovisionedSite( site: SiteData ): Promise< boolean > { } const [ hasConfig, hasDatabase ] = await Promise.all( [ pathExists( path.join( site.path, 'wp-config.php' ) ), - pathExists( path.join( site.path, 'wp-content', 'database', '.ht.sqlite' ) ), + pathExists( getSiteDatabasePath( site.path ) ), ] ); return ! hasConfig && ! hasDatabase; } diff --git a/apps/cli/commands/export.ts b/apps/cli/commands/export.ts index 525cfa9e1c..8e8d45a00a 100644 --- a/apps/cli/commands/export.ts +++ b/apps/cli/commands/export.ts @@ -10,8 +10,8 @@ import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { ImportExportEventEmitter } from 'cli/lib/import-export/events'; import { getExporter } from 'cli/lib/import-export/export/export-manager'; import { ExportOptions } from 'cli/lib/import-export/export/types'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { withSiteOperation } from 'cli/lib/site-operations'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { getTracksOrigin, recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks'; import { classifyExportFailure, untildify } from 'cli/lib/utils'; import { Logger, LoggerError } from 'cli/logger'; @@ -169,7 +169,7 @@ async function exportSite( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration, if needed…' ) ); - await keepSqliteIntegrationUpdated( siteFolder ); + await ensureSiteDatabaseReady( siteFolder ); logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); const includes: ExportOptions[ 'includes' ] = { database: true, wpContent: true }; diff --git a/apps/cli/commands/import.ts b/apps/cli/commands/import.ts index 621a3892a1..7cb54f9c1c 100644 --- a/apps/cli/commands/import.ts +++ b/apps/cli/commands/import.ts @@ -24,8 +24,8 @@ import { connectToDaemon, disconnectFromDaemon, emitCliEvent } from 'cli/lib/dae import { ImportExportEventEmitter } from 'cli/lib/import-export/events'; import { DEFAULT_IMPORTER_OPTIONS, getImporter } from 'cli/lib/import-export/import/import-manager'; import { getBackupFileType } from 'cli/lib/import-export/utils'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { withSiteOperation } from 'cli/lib/site-operations'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { reportSyncActivity } from 'cli/lib/sync-activity'; import { getTracksOrigin, recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks'; import { classifyImportFailure, untildify } from 'cli/lib/utils'; @@ -366,7 +366,7 @@ async function importBackup( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration, if needed…' ) ); - await keepSqliteIntegrationUpdated( siteFolder ); + await ensureSiteDatabaseReady( siteFolder ); logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); logger.reportStart( LoggerAction.START_SITE, __( 'Starting WordPress server…' ) ); diff --git a/apps/cli/commands/pull-reprint.ts b/apps/cli/commands/pull-reprint.ts index f1429742d1..0bedef82e4 100644 --- a/apps/cli/commands/pull-reprint.ts +++ b/apps/cli/commands/pull-reprint.ts @@ -11,6 +11,7 @@ import os from 'os'; import path from 'path'; import { encodePassword } from '@studio/common/lib/passwords'; import { readAuthToken, type StoredAuthToken } from '@studio/common/lib/shared-config'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { PullReprintCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions'; import { __, sprintf } from '@wordpress/i18n'; import chalk from 'chalk'; @@ -608,9 +609,9 @@ async function applySelection( params: { ? path.join( session.rawDirectory, ...contentDir.split( '/' ).filter( Boolean ) ) : path.join( session.rawDirectory, 'wp-content' ); const candidates = [ - path.join( session.sitePath, 'wp-content', 'database', '.ht.sqlite' ), - path.join( rawContentPath, 'database', '.ht.sqlite' ), - path.join( session.rawDirectory, 'wp-content', 'database', '.ht.sqlite' ), + getSiteDatabasePath( session.sitePath ), + path.join( rawContentPath, path.relative( 'wp-content', getSiteDatabasePath( '' ) ) ), + getSiteDatabasePath( session.rawDirectory ), ]; if ( ! candidates.some( ( candidate ) => fs.existsSync( candidate ) ) ) { throw new LoggerError( @@ -903,21 +904,20 @@ export async function runFullPull( importedSqlitePath = path.join( metadata.rawDirectory, ...contentDir.split( '/' ).filter( Boolean ), - 'database', - '.ht.sqlite' + path.relative( 'wp-content', getSiteDatabasePath( '' ) ) ); } else if ( selection.skipDatabase ) { - importedSqlitePath = path.join( metadata.sitePath, 'wp-content', 'database', '.ht.sqlite' ); + importedSqlitePath = getSiteDatabasePath( metadata.sitePath ); } else { - importedSqlitePath = path.join( metadata.rawDirectory, 'wp-content', 'database', '.ht.sqlite' ); + importedSqlitePath = getSiteDatabasePath( metadata.rawDirectory ); } let sqlitePath = importedSqlitePath; if ( selection.skipDatabase ) { const existingSqlitePath = [ importedSqlitePath, - path.join( metadata.sitePath, 'wp-content', 'database', '.ht.sqlite' ), - path.join( metadata.rawDirectory, 'wp-content', 'database', '.ht.sqlite' ), + getSiteDatabasePath( metadata.sitePath ), + getSiteDatabasePath( metadata.rawDirectory ), ].find( ( candidate ) => fs.existsSync( candidate ) ); if ( existingSqlitePath ) { sqlitePath = existingSqlitePath; diff --git a/apps/cli/commands/push.ts b/apps/cli/commands/push.ts index 71a17b7c6e..64a814aea6 100644 --- a/apps/cli/commands/push.ts +++ b/apps/cli/commands/push.ts @@ -23,8 +23,8 @@ import { __, sprintf } from '@wordpress/i18n'; import { getSiteByFolder } from 'cli/lib/cli-config/sites'; import { getExporter } from 'cli/lib/import-export/export/export-manager'; import { ExportOptions } from 'cli/lib/import-export/export/types'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { withSiteOperation } from 'cli/lib/site-operations'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { exitOnCancel, reportSyncActivity } from 'cli/lib/sync-activity'; import { fetchSyncableSites, @@ -134,7 +134,7 @@ async function runPush( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration, if needed…' ) ); - await keepSqliteIntegrationUpdated( siteFolder ); + await ensureSiteDatabaseReady( siteFolder ); logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); logger.reportStart( LoggerAction.FETCH_REMOTE_SITES, __( 'Fetching WordPress.com sites…' ) ); diff --git a/apps/cli/commands/site/create.ts b/apps/cli/commands/site/create.ts index 7b8fd3b490..b5545893dc 100644 --- a/apps/cli/commands/site/create.ts +++ b/apps/cli/commands/site/create.ts @@ -69,11 +69,11 @@ import { updateServerFiles } from 'cli/lib/dependency-management/setup'; import { downloadWordPress } from 'cli/lib/dependency-management/wordpress'; import { copyLanguagePackToSite } from 'cli/lib/language-packs'; import { validateSupportedPhpVersion } from 'cli/lib/php-versions'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { getPreferredSiteLanguage } from 'cli/lib/site-language'; import { generateSiteName } from 'cli/lib/site-name'; import { getDefaultSitePath } from 'cli/lib/site-paths'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { completeSourceImport, prepareSourceImport, @@ -290,7 +290,7 @@ export async function runCommand( } logger.reportStart( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration…' ) ); - await keepSqliteIntegrationUpdated( sitePath ); + await ensureSiteDatabaseReady( sitePath ); logger.reportSuccess( __( 'SQLite integration configured' ) ); try { diff --git a/apps/cli/commands/site/start.ts b/apps/cli/commands/site/start.ts index 61935a8d35..713b3ecb0c 100644 --- a/apps/cli/commands/site/start.ts +++ b/apps/cli/commands/site/start.ts @@ -5,9 +5,9 @@ import { __, sprintf } from '@wordpress/i18n'; import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { getAiInstructionsPath } from 'cli/lib/dependency-management/paths'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { withSiteOperation } from 'cli/lib/site-operations'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager'; import { Logger, LoggerError } from 'cli/logger'; import { StudioArgv } from 'cli/types'; @@ -81,7 +81,7 @@ async function startSite( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration, if needed…' ) ); - await keepSqliteIntegrationUpdated( sitePath ); + await ensureSiteDatabaseReady( sitePath ); logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); try { diff --git a/apps/cli/commands/site/tests/create.test.ts b/apps/cli/commands/site/tests/create.test.ts index 54b529d457..4000f2da8c 100644 --- a/apps/cli/commands/site/tests/create.test.ts +++ b/apps/cli/commands/site/tests/create.test.ts @@ -37,12 +37,9 @@ import { updateServerFiles } from 'cli/lib/dependency-management/setup'; import { downloadWordPress } from 'cli/lib/dependency-management/wordpress'; import { copyLanguagePackToSite } from 'cli/lib/language-packs'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; +import { isSqliteIntegrationAvailable, ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { getPreferredSiteLanguage } from 'cli/lib/site-language'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { - isSqliteIntegrationAvailable, - keepSqliteIntegrationUpdated, -} from 'cli/lib/sqlite-integration'; import { buildCreateFromSourceBlueprint } from 'cli/lib/static-site-import/source'; import { recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks'; import { ProcessDescription } from 'cli/lib/types/process-manager-ipc'; @@ -96,7 +93,7 @@ vi.mock( import( '@studio/common/lib/well-known-paths' ), async ( importOriginal vi.mock( 'cli/lib/site-language' ); vi.mock( 'cli/lib/site-utils' ); vi.mock( '@studio/common/lib/agent-skills' ); -vi.mock( 'cli/lib/sqlite-integration' ); +vi.mock( 'cli/lib/site-database' ); vi.mock( 'cli/lib/run-wp-cli-command' ); vi.mock( 'cli/lib/wordpress-server-manager' ); const { staticSiteImporterFixture } = vi.hoisted( () => { @@ -254,7 +251,7 @@ describe( 'CLI: studio create', () => { vi.mocked( saveCliConfig ).mockResolvedValue( undefined ); vi.mocked( lockCliConfig ).mockResolvedValue( undefined ); vi.mocked( unlockCliConfig ).mockResolvedValue( undefined ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( undefined ); + vi.mocked( ensureSiteDatabaseReady ).mockResolvedValue( undefined ); vi.mocked( isSqliteIntegrationAvailable ).mockResolvedValue( true ); vi.mocked( connectToDaemon ).mockResolvedValue( undefined ); vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined ); @@ -449,7 +446,7 @@ describe( 'CLI: studio create', () => { } ); it( 'should error if SQLite integration is not available', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( + vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'SQLite integration files not found. Please ensure Studio is installed.' ) ); @@ -1232,7 +1229,7 @@ describe( 'CLI: studio create', () => { await runCommand( mockSitePath, { ...defaultTestOptions } ); expect( fsMkdirSyncSpy ).toHaveBeenCalledWith( mockSitePath, { recursive: true } ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( mockSitePath ); + expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( mockSitePath ); expect( loggerReportSuccessSpy ).toHaveBeenCalledWith( 'SQLite integration configured' ); expect( portFinder.getOpenPort ).toHaveBeenCalled(); expect( lockCliConfig ).toHaveBeenCalled(); @@ -1815,9 +1812,7 @@ describe( 'CLI: studio create', () => { } ); it( 'should handle SQLite setup failure', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( - new Error( 'SQLite setup failed' ) - ); + vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'SQLite setup failed' ) ); await expect( runCommand( mockSitePath, { ...defaultTestOptions } ) ).rejects.toThrow(); diff --git a/apps/cli/commands/site/tests/start.test.ts b/apps/cli/commands/site/tests/start.test.ts index 3451c85122..f332fcf564 100644 --- a/apps/cli/commands/site/tests/start.test.ts +++ b/apps/cli/commands/site/tests/start.test.ts @@ -2,8 +2,8 @@ import { vi } from 'vitest'; import { SiteData } from 'cli/lib/cli-config/core'; import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { ProcessDescription } from 'cli/lib/types/process-manager-ipc'; import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager'; import { Logger } from 'cli/logger'; @@ -24,7 +24,7 @@ vi.mock( 'cli/lib/site-operations', async ( importOriginal ) => ( { } ) ); vi.mock( 'cli/lib/site-utils' ); vi.mock( 'cli/lib/wordpress-server-manager' ); -vi.mock( 'cli/lib/sqlite-integration' ); +vi.mock( 'cli/lib/site-database' ); describe( 'CLI: studio site start', () => { // Simple test data @@ -59,7 +59,7 @@ describe( 'CLI: studio site start', () => { vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined ); vi.mocked( isServerRunning ).mockResolvedValue( undefined ); vi.mocked( setupCustomDomain ).mockResolvedValue( undefined ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( undefined ); + vi.mocked( ensureSiteDatabaseReady ).mockResolvedValue( undefined ); vi.mocked( startWordPressServer ).mockResolvedValue( testProcessDescription ); vi.mocked( updateSiteLatestCliPid ).mockResolvedValue( undefined ); vi.mocked( logSiteDetails ).mockImplementation( () => {} ); @@ -126,9 +126,7 @@ describe( 'CLI: studio site start', () => { } ); it( 'should throw when SQLite integration setup fails', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( - new Error( 'SQLite setup failed' ) - ); + vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'SQLite setup failed' ) ); await expect( runCommand( '/test/site' ) ).rejects.toThrow( 'SQLite setup failed' ); expect( disconnectFromDaemon ).toHaveBeenCalled(); @@ -158,7 +156,7 @@ describe( 'CLI: studio site start', () => { expect( connectToDaemon ).toHaveBeenCalled(); expect( isServerRunning ).toHaveBeenCalledWith( testSite.id ); expect( setupCustomDomain ).toHaveBeenCalledWith( testSite, expect.any( Logger ) ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( '/test/site' ); + expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( '/test/site' ); expect( startWordPressServer ).toHaveBeenCalledWith( testSite, expect.any( Logger ) ); expect( logSiteDetails ).toHaveBeenCalledWith( testSite ); expect( openSiteInBrowser ).toHaveBeenCalledWith( testSite ); @@ -197,7 +195,7 @@ describe( 'CLI: studio site start', () => { it( 'should update SQLite integration', async () => { await runCommand( '/test/site' ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( '/test/site' ); + expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( '/test/site' ); } ); it( 'should skip browser when skipBrowser is true', async () => { diff --git a/apps/cli/commands/tests/export.test.ts b/apps/cli/commands/tests/export.test.ts index 24094aba40..c760a81f25 100644 --- a/apps/cli/commands/tests/export.test.ts +++ b/apps/cli/commands/tests/export.test.ts @@ -6,8 +6,8 @@ import { getSiteByFolder } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { ImportExportEventEmitter } from 'cli/lib/import-export/events'; import { getExporter } from 'cli/lib/import-export/export/export-manager'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { withSiteOperation } from 'cli/lib/site-operations'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks'; import { Logger, LoggerError } from 'cli/logger'; import { registerCommand, runCommand } from '../export'; @@ -29,7 +29,7 @@ vi.mock( 'cli/lib/cli-config/sites', () => ( { getSiteByFolder: vi.fn(), } ) ); vi.mock( 'cli/lib/daemon-client' ); -vi.mock( 'cli/lib/sqlite-integration' ); +vi.mock( 'cli/lib/site-database' ); vi.mock( import( 'cli/lib/import-export/export/export-manager' ), () => ( { getExporter: vi.fn(), } ) ); @@ -79,7 +79,7 @@ describe( 'CLI: studio export', () => { vi.mocked( connectToDaemon ).mockResolvedValue( undefined ); vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined ); vi.mocked( getSiteByFolder ).mockResolvedValue( testSite ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( undefined ); + vi.mocked( ensureSiteDatabaseReady ).mockResolvedValue( undefined ); vi.mocked( getExporter ).mockResolvedValue( createExporter() as never ); } ); @@ -93,7 +93,7 @@ describe( 'CLI: studio export', () => { expect( connectToDaemon ).toHaveBeenCalled(); expect( getSiteByFolder ).toHaveBeenCalledWith( testSitePath ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( testSitePath ); + expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( testSitePath ); expect( getExporter ).toHaveBeenCalledWith( { site: testSite, backupFile: testExportPath, diff --git a/apps/cli/commands/tests/import.test.ts b/apps/cli/commands/tests/import.test.ts index e983d62fce..56d9165544 100644 --- a/apps/cli/commands/tests/import.test.ts +++ b/apps/cli/commands/tests/import.test.ts @@ -13,8 +13,8 @@ import { getSiteByFolder, updateSitePhpVersion } from 'cli/lib/cli-config/sites' import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { ImportExportEventEmitter } from 'cli/lib/import-export/events'; import { DEFAULT_IMPORTER_OPTIONS, getImporter } from 'cli/lib/import-export/import/import-manager'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { withSiteOperation } from 'cli/lib/site-operations'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { reportSyncActivity } from 'cli/lib/sync-activity'; import { recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks'; import { isServerRunning, stopWordPressServer } from 'cli/lib/wordpress-server-manager'; @@ -29,8 +29,8 @@ vi.mock( 'cli/lib/cli-config/sites', () => ( { updateSitePhpVersion: vi.fn(), } ) ); vi.mock( 'cli/lib/daemon-client' ); -vi.mock( 'cli/lib/sqlite-integration', () => ( { - keepSqliteIntegrationUpdated: vi.fn(), +vi.mock( 'cli/lib/site-database', () => ( { + ensureSiteDatabaseReady: vi.fn(), } ) ); vi.mock( 'cli/lib/wordpress-server-manager', () => ( { isServerRunning: vi.fn(), @@ -101,7 +101,7 @@ describe( 'CLI: studio import', () => { vi.mocked( getSiteByFolder ).mockResolvedValue( testSite ); vi.mocked( isServerRunning ).mockResolvedValue( undefined ); vi.mocked( stopWordPressServer ).mockResolvedValue( undefined ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( undefined ); + vi.mocked( ensureSiteDatabaseReady ).mockResolvedValue( undefined ); vi.mocked( isWordPressDirectory ).mockReturnValue( true ); vi.mocked( getServerFilesPath ).mockReturnValue( '/server-files' ); vi.mocked( recursiveCopyDirectory ).mockResolvedValue( undefined ); @@ -232,7 +232,7 @@ describe( 'CLI: studio import', () => { throw new Error( 'import failed' ); } ) as never ); - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( new Error( 'restart failed' ) ); + vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'restart failed' ) ); await expect( runCommand( testSitePath, testImportPath ) ).rejects.toThrow( 'import failed' ); expect( stopWordPressServer ).toHaveBeenCalledWith( testSite.id ); @@ -249,7 +249,7 @@ describe( 'CLI: studio import', () => { ); } ) as never ); - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( new Error( 'restart failed' ) ); + vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'restart failed' ) ); await expect( runCommand( testSitePath, testImportPath ) ).rejects.toThrow( 'Failed to extract backup: unexpected end of file' @@ -336,7 +336,7 @@ describe( 'CLI: studio import', () => { throw new LoggerError( 'Database import failed: x', undefined, 'database_import' ); } ) as never ); - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( new Error( 'restart failed' ) ); + vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'restart failed' ) ); await expect( runCommand( testSitePath, testImportPath ) ).rejects.toThrow( 'Database import failed' diff --git a/apps/cli/commands/tests/push.test.ts b/apps/cli/commands/tests/push.test.ts index 1b4fdcd08f..5c1ab7a0f9 100644 --- a/apps/cli/commands/tests/push.test.ts +++ b/apps/cli/commands/tests/push.test.ts @@ -30,7 +30,7 @@ vi.mock( 'cli/lib/import-export/export/export-manager' ); vi.mock( 'cli/lib/site-operations', () => ( { withSiteOperation: ( _folder: string, _kind: string, fn: () => unknown ) => fn(), } ) ); -vi.mock( 'cli/lib/sqlite-integration' ); +vi.mock( 'cli/lib/site-database' ); vi.mock( 'cli/lib/sync-activity' ); vi.mock( 'cli/lib/sync-api', async ( importActual ) => ( { ...( await importActual< typeof import('cli/lib/sync-api') >() ), diff --git a/apps/cli/commands/tests/sync-tracks.test.ts b/apps/cli/commands/tests/sync-tracks.test.ts index c0552e795b..92e6f1bd15 100644 --- a/apps/cli/commands/tests/sync-tracks.test.ts +++ b/apps/cli/commands/tests/sync-tracks.test.ts @@ -23,7 +23,7 @@ vi.mock( 'cli/lib/cli-config/sites', async ( importActual ) => ( { getSiteByFolder: vi.fn(), } ) ); vi.mock( 'cli/lib/daemon-client' ); -vi.mock( 'cli/lib/sqlite-integration' ); +vi.mock( 'cli/lib/site-database' ); vi.mock( 'cli/lib/sync-api', async ( importActual ) => ( { ...( await importActual< typeof import('cli/lib/sync-api') >() ), fetchSyncableSites: vi.fn(), diff --git a/apps/cli/lib/import-export/export/exporters/default-exporter.ts b/apps/cli/lib/import-export/export/exporters/default-exporter.ts index 069eebd916..1dcd3b8245 100644 --- a/apps/cli/lib/import-export/export/exporters/default-exporter.ts +++ b/apps/cli/lib/import-export/export/exporters/default-exporter.ts @@ -21,10 +21,10 @@ import { glob } from 'glob'; import { getSiteUrl } from 'cli/lib/cli-config/sites'; import { getWordPressVersionFromInstallation } from 'cli/lib/dependency-management/wordpress'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; -import { ensureSqliteIntegrationForImportedSite } from 'cli/lib/sqlite-integration'; +import { ensureSqliteIntegrationForImportedSite } from 'cli/lib/site-database'; +import { dumpDatabase } from 'cli/lib/site-database/dump-load'; import { LoggerError } from 'cli/logger'; import { ImportExportEventEmitter } from '../../events'; -import { exportDatabaseToFile, exportDatabaseToMultipleFiles } from '../export-database'; import { ExportOptions, BackupContents, @@ -289,7 +289,7 @@ export class DefaultExporter extends ImportExportEventEmitter implements Exporte const tmpFolder = await fsPromises.mkdtemp( path.join( os.tmpdir(), 'studio_export' ) ); if ( this.options.splitDatabaseDumpByTable ) { - const sqlFiles = await exportDatabaseToMultipleFiles( this.options.site, tmpFolder ); + const sqlFiles = await dumpDatabase( this.options.site, tmpFolder, true ); sqlFiles.forEach( ( file ) => this.archiveBuilder.file( file, { name: `sql/${ path.basename( file ) }` } ) ); @@ -297,7 +297,7 @@ export class DefaultExporter extends ImportExportEventEmitter implements Exporte } else { const fileName = `${ generateBackupFilename( 'db-export' ) }.sql`; const sqlDumpPath = path.join( tmpFolder, fileName ); - await exportDatabaseToFile( this.options.site, sqlDumpPath ); + await dumpDatabase( this.options.site, sqlDumpPath ); this.archiveBuilder.file( sqlDumpPath, { name: `sql/${ fileName }` } ); this.backup.sqlFiles.push( sqlDumpPath ); } diff --git a/apps/cli/lib/import-export/export/exporters/sql-exporter.ts b/apps/cli/lib/import-export/export/exporters/sql-exporter.ts index e3719799b4..61558c9502 100644 --- a/apps/cli/lib/import-export/export/exporters/sql-exporter.ts +++ b/apps/cli/lib/import-export/export/exporters/sql-exporter.ts @@ -1,6 +1,6 @@ import { createExportErrorPayload, ExportEvents } from '@studio/common/lib/import-export-events'; +import { dumpDatabase } from 'cli/lib/site-database/dump-load'; import { ImportExportEventEmitter } from '../../events'; -import { exportDatabaseToFile } from '../export-database'; import { ExportOptions, Exporter } from '../types'; export class SqlExporter extends ImportExportEventEmitter implements Exporter { @@ -10,7 +10,7 @@ export class SqlExporter extends ImportExportEventEmitter implements Exporter { async export(): Promise< void > { this.emit( ExportEvents.EXPORT_START ); try { - await exportDatabaseToFile( this.options.site, this.options.backupFile ); + await dumpDatabase( this.options.site, this.options.backupFile ); this.emit( ExportEvents.EXPORT_COMPLETE ); } catch ( error ) { this.emit( ExportEvents.EXPORT_ERROR, createExportErrorPayload( error ) ); diff --git a/apps/cli/lib/import-export/import/importers/importer.ts b/apps/cli/lib/import-export/import/importers/importer.ts index d4d0b5d2c2..92bc8b9e36 100644 --- a/apps/cli/lib/import-export/import/importers/importer.ts +++ b/apps/cli/lib/import-export/import/importers/importer.ts @@ -1,26 +1,23 @@ import fs from 'fs'; import path from 'path'; import { createInterface } from 'readline'; -import { generateBackupFilename } from '@studio/common/lib/generate-backup-filename'; import { ImportEvents } from '@studio/common/lib/import-export-events'; import { isErrnoException } from '@studio/common/lib/is-errno-exception'; import { writeStudioMuPlugins } from '@studio/common/lib/mu-plugins'; import { serializePlugins } from '@studio/common/lib/serialize-plugins'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { RecommendedPHPVersion, SupportedPHPVersions, type SupportedPHPVersion, } from '@studio/common/types/php-versions'; -import { __, sprintf } from '@wordpress/i18n'; import { move } from 'fs-extra'; import semver from 'semver'; import trash from 'trash'; import { SiteData } from 'cli/lib/cli-config/core'; import { ensureWpConfig } from 'cli/lib/native-php/site-setup'; -import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; -import { summarizeWpCliStderr } from 'cli/lib/wp-cli-stderr'; -import { LoggerError } from 'cli/logger'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; +import { loadDatabase } from 'cli/lib/site-database/dump-load'; import { ImportExportEventEmitter } from '../../events'; import { BackupContents, MetaFileData } from '../types'; import { updateSiteUrl } from '../update-site-url'; @@ -92,59 +89,17 @@ abstract class BaseImporter extends ImportExportEventEmitter implements Importer this.emit( ImportEvents.IMPORT_DATABASE_START ); - const sortedSqlFiles = sqlFiles.sort( ( a, b ) => a.localeCompare( b ) ); - let processedFiles = 0; - const totalFiles = sortedSqlFiles.length; - - for ( const sqlFile of sortedSqlFiles ) { - const sqlTempFile = `${ generateBackupFilename( 'sql' ) }.sql`; - const tmpPath = path.join( site.path, sqlTempFile ); - processedFiles++; - - this.emit( ImportEvents.IMPORT_DATABASE_PROGRESS, { - currentFile: path.basename( sqlFile ), - processedFiles, - totalFiles, - } ); - - try { - await move( sqlFile, tmpPath ); - await this.prepareSqlFile( tmpPath ); - - await using command = await runWpCliCommand( - site, - [ - 'sqlite', - 'import', - sqlTempFile, - '--enable-ast-driver', - '--skip-plugins', - '--skip-themes', - ], - { - requireSqliteCliCommand: true, - phpVersion: this.resolvePhpVersion( site ), - } - ); - - const exitCode = await command.response.exitCode; - const stderr = await command.response.stderrText; - - if ( stderr ) { - console.error( sprintf( __( 'Error during import of %s:' ), sqlFile ), stderr ); - } - - if ( exitCode !== 0 ) { - throw new LoggerError( - sprintf( __( 'Database import failed: %s' ), summarizeWpCliStderr( stderr ) ), - undefined, - 'database_import' - ); - } - } finally { - await this.safelyDeletePath( tmpPath ); - } - } + await loadDatabase( site, sqlFiles, { + phpVersion: this.resolvePhpVersion( site ), + prepareSqlFile: ( file ) => this.prepareSqlFile( file ), + onProgress: ( sqlFile, processedFiles, totalFiles ) => { + this.emit( ImportEvents.IMPORT_DATABASE_PROGRESS, { + currentFile: path.basename( sqlFile ), + processedFiles, + totalFiles, + } ); + }, + } ); await updateSiteUrl( site ); this.emit( ImportEvents.IMPORT_DATABASE_COMPLETE ); @@ -177,8 +132,7 @@ abstract class BaseBackupImporter extends BaseImporter { await this.importWpConfig( site ); await this.importWpContent( site ); if ( this.backup.sqlFiles.length ) { - const databaseDir = path.join( site.path, 'wp-content', 'database' ); - const dbPath = path.join( databaseDir, '.ht.sqlite' ); + const dbPath = getSiteDatabasePath( site.path ); await this.moveExistingDatabaseToTrash( dbPath ); await this.createEmptyDatabase( dbPath ); @@ -307,7 +261,7 @@ abstract class BaseBackupImporter extends BaseImporter { } } - await keepSqliteIntegrationUpdated( site.path ); + await ensureSiteDatabaseReady( site.path ); await writeStudioMuPlugins( site.path, site.isWpAutoUpdating ); this.emit( ImportEvents.IMPORT_WP_CONTENT_COMPLETE ); @@ -409,7 +363,7 @@ export class PlaygroundImporter extends BaseBackupImporter { this.emit( ImportEvents.IMPORT_DATABASE_START ); for ( const sqlFile of sqlFiles ) { - await move( sqlFile, path.join( site.path, 'wp-content', 'database', '.ht.sqlite' ), { + await move( sqlFile, getSiteDatabasePath( site.path ), { overwrite: true, } ); } diff --git a/apps/cli/lib/import-export/import/validators/playground-validator.ts b/apps/cli/lib/import-export/import/validators/playground-validator.ts index 4bd8822cd7..d2f2460f15 100644 --- a/apps/cli/lib/import-export/import/validators/playground-validator.ts +++ b/apps/cli/lib/import-export/import/validators/playground-validator.ts @@ -1,8 +1,17 @@ import path from 'path'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { ImportExportEventEmitter } from '../../events'; import { BackupContents } from '../types'; import { Validator } from './validator'; +function isDatabaseFile( file: string ): boolean { + const databasePath = getSiteDatabasePath( '' ).split( path.sep ).join( '/' ); + return ( + file.startsWith( path.posix.dirname( databasePath ) ) && + file.endsWith( path.posix.basename( databasePath ) ) + ); +} + export class PlaygroundValidator extends ImportExportEventEmitter implements Validator { canHandle( fileList: string[] ): boolean { const requiredDirs = [ @@ -13,9 +22,7 @@ export class PlaygroundValidator extends ImportExportEventEmitter implements Val ]; const hasSqliteDatabase = requiredDirs.some( ( dir ) => fileList.some( ( file ) => file.startsWith( dir + '/' ) ) ) && - fileList.some( - ( file ) => file.startsWith( 'wp-content/database' ) && file.endsWith( '.ht.sqlite' ) - ); + fileList.some( isDatabaseFile ); const hasSqlDumps = fileList.some( ( file ) => file.startsWith( 'sql/' ) ); @@ -44,7 +51,7 @@ export class PlaygroundValidator extends ImportExportEventEmitter implements Val continue; } - if ( file.startsWith( 'wp-content/database' ) && file.endsWith( '.ht.sqlite' ) ) { + if ( isDatabaseFile( file ) ) { extractedBackup.sqlFiles.push( fullPath ); } else if ( file.startsWith( 'wp-content/' ) ) { extractedBackup.wpContentFiles.push( fullPath ); diff --git a/apps/cli/lib/native-php/blueprints.ts b/apps/cli/lib/native-php/blueprints.ts index 8f285b5690..f49a3c63c4 100644 --- a/apps/cli/lib/native-php/blueprints.ts +++ b/apps/cli/lib/native-php/blueprints.ts @@ -4,6 +4,7 @@ import { createBlueprintTempDir, removeBlueprintTempDir, } from '@studio/common/lib/blueprint-bundle'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { parse as parseShellCommand } from 'shell-quote'; import { @@ -12,7 +13,7 @@ import { getWpCliPharPath, } from 'cli/lib/dependency-management/paths'; import { getFullyResolvedTmpDirPath } from 'cli/lib/native-php/tmp-dir'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; import { getWpCliPhpIniArgs } from 'cli/lib/wp-cli-php-ini'; import { PhpCommandError, runPhpCommand } from './php-process'; import type { SupportedPHPVersion } from '@studio/common/types/php-versions'; @@ -314,7 +315,7 @@ export async function runBlueprint( `--site-path=${ config.sitePath }`, `--site-url=${ config.absoluteUrl ?? `http://localhost:${ config.port }` }`, '--db-engine=sqlite', - `--db-path=${ path.join( config.sitePath, 'wp-content', 'database', '.ht.sqlite' ) }`, + `--db-path=${ getSiteDatabasePath( config.sitePath ) }`, ], { phpVersion, @@ -345,7 +346,7 @@ export async function runBlueprint( if ( needsSymlink ) { await removeOwnedSqliteSymlink( pluginsSqlite, symlinkIno! ); // The runner may remove the symlink target while managing its SQLite driver. - await keepSqliteIntegrationUpdated( config.sitePath ); + await ensureSiteDatabaseReady( config.sitePath ); } } } diff --git a/apps/cli/lib/native-php/phpmyadmin.ts b/apps/cli/lib/native-php/phpmyadmin.ts index d07d97cc55..7863be0d67 100644 --- a/apps/cli/lib/native-php/phpmyadmin.ts +++ b/apps/cli/lib/native-php/phpmyadmin.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { getFullyResolvedTmpDirPath } from './tmp-dir'; import type { ServerConfig } from 'cli/lib/types/wordpress-server-ipc'; @@ -33,7 +34,7 @@ export async function writeNativePhpMyAdminWpEnv( config: ServerConfig ): Promis 'database', 'load.php' ); - const sqliteDatabasePath = path.join( config.sitePath, 'wp-content', 'database', '.ht.sqlite' ); + const sqliteDatabasePath = getSiteDatabasePath( config.sitePath ); const wpEnvPhp = ` array ( 'type' => 'sqlite', diff --git a/apps/cli/lib/native-php/tests/blueprints.test.ts b/apps/cli/lib/native-php/tests/blueprints.test.ts index 41fd61ed1c..82541a6cd8 100644 --- a/apps/cli/lib/native-php/tests/blueprints.test.ts +++ b/apps/cli/lib/native-php/tests/blueprints.test.ts @@ -196,7 +196,8 @@ describe( 'removeOwnedSqliteSymlink', () => { fs.mkdirSync( target ); fs.symlinkSync( target, symlink, 'junction' ); const symlinkIno = fs.lstatSync( symlink ).ino; - fs.rmSync( symlink ); + // Keep the old inode allocated so the replacement cannot reuse it. + fs.renameSync( symlink, path.join( tempDir, 'original-link' ) ); fs.mkdirSync( symlink ); await removeOwnedSqliteSymlink( symlink, symlinkIno ); diff --git a/apps/cli/lib/import-export/export/export-database.ts b/apps/cli/lib/site-database/dump-load.ts similarity index 65% rename from apps/cli/lib/import-export/export/export-database.ts rename to apps/cli/lib/site-database/dump-load.ts index b45413f3fc..9f758521af 100644 --- a/apps/cli/lib/import-export/export/export-database.ts +++ b/apps/cli/lib/site-database/dump-load.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import path from 'path'; import { DEFAULT_PHP_VERSION } from '@studio/common/constants'; import { generateBackupFilename } from '@studio/common/lib/generate-backup-filename'; @@ -8,6 +9,7 @@ import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; import { replaceMysql8OnlyCollations } from 'cli/lib/sqlite-collations'; import { summarizeWpCliStderr } from 'cli/lib/wp-cli-stderr'; import { LoggerError } from 'cli/logger'; +import type { SupportedPHPVersion } from '@studio/common/types/php-versions'; import type { SiteData } from 'cli/lib/cli-config/core'; async function createDatabaseExportError( @@ -22,10 +24,7 @@ async function createDatabaseExportError( return new LoggerError( message, new Error( summarizeWpCliStderr( stderr ) ), 'database_export' ); } -export async function exportDatabaseToFile( - site: SiteData, - finalDestination: string -): Promise< void > { +async function exportDatabaseToFile( site: SiteData, finalDestination: string ): Promise< void > { // Generate a temporary file name in the project directory const tempFileName = `${ generateBackupFilename( 'db-export' ) }.sql`; @@ -55,7 +54,7 @@ export async function exportDatabaseToFile( await move( tempFilePath, finalDestination ); } -export async function exportDatabaseToMultipleFiles( +async function exportDatabaseToMultipleFiles( site: SiteData, finalDestinationDir: string ): Promise< string[] > { @@ -147,3 +146,67 @@ export async function exportDatabaseToMultipleFiles( return tmpFiles; } + +export function dumpDatabase( site: SiteData, destination: string ): Promise< void >; +export function dumpDatabase( + site: SiteData, + destination: string, + separateTables: true +): Promise< string[] >; +export function dumpDatabase( site: SiteData, destination: string, separateTables = false ) { + return separateTables + ? exportDatabaseToMultipleFiles( site, destination ) + : exportDatabaseToFile( site, destination ); +} + +export async function loadDatabase( + site: SiteData, + files: string[], + options: { + phpVersion: SupportedPHPVersion; + prepareSqlFile: ( file: string ) => Promise< void >; + onProgress: ( file: string, processedFiles: number, totalFiles: number ) => void; + } +): Promise< void > { + const sortedFiles = files.sort( ( a, b ) => a.localeCompare( b ) ); + let processedFiles = 0; + for ( const sqlFile of sortedFiles ) { + const sqlTempFile = `${ generateBackupFilename( 'sql' ) }.sql`; + const tmpPath = path.join( site.path, sqlTempFile ); + options.onProgress( sqlFile, ++processedFiles, sortedFiles.length ); + try { + await move( sqlFile, tmpPath ); + await options.prepareSqlFile( tmpPath ); + await using command = await runWpCliCommand( + site, + [ + 'sqlite', + 'import', + sqlTempFile, + '--enable-ast-driver', + '--skip-plugins', + '--skip-themes', + ], + { requireSqliteCliCommand: true, phpVersion: options.phpVersion } + ); + const exitCode = await command.response.exitCode; + const stderr = await command.response.stderrText; + if ( stderr ) { + console.error( sprintf( __( 'Error during import of %s:' ), sqlFile ), stderr ); + } + if ( exitCode !== 0 ) { + throw new LoggerError( + sprintf( __( 'Database import failed: %s' ), summarizeWpCliStderr( stderr ) ), + undefined, + 'database_import' + ); + } + } finally { + try { + await fs.promises.rm( tmpPath, { recursive: true, force: true } ); + } catch ( error ) { + console.error( `Failed to safely delete path ${ tmpPath }:`, error ); + } + } + } +} diff --git a/apps/cli/lib/sqlite-integration.ts b/apps/cli/lib/site-database/index.ts similarity index 85% rename from apps/cli/lib/sqlite-integration.ts rename to apps/cli/lib/site-database/index.ts index 68502522b0..eaa9078e46 100644 --- a/apps/cli/lib/sqlite-integration.ts +++ b/apps/cli/lib/site-database/index.ts @@ -20,14 +20,14 @@ export async function isSqliteIntegrationAvailable() { return provider.isSqliteIntegrationAvailable(); } -export async function keepSqliteIntegrationUpdated( sitePath: string ) { - return provider.keepSqliteIntegrationUpdated( sitePath ); +export async function ensureSiteDatabaseReady( sitePath: string ) { + return provider.ensureSiteDatabaseReady( sitePath ); } /** * Installs the SQLite integration for imported (reprint-pulled) sites when it's * missing. Those sites wire SQLite through runtime.php and ship no db.php drop-in, - * so keepSqliteIntegrationUpdated skips them — but tools that need the integration + * so ensureSiteDatabaseReady skips them — but tools that need the integration * discoverable in wp-content (e.g. `wp sqlite export`) can call this first. * No-op for regular sites. */ diff --git a/apps/cli/lib/import-export/export/tests/export-database.test.ts b/apps/cli/lib/site-database/tests/dump.test.ts similarity index 75% rename from apps/cli/lib/import-export/export/tests/export-database.test.ts rename to apps/cli/lib/site-database/tests/dump.test.ts index b2b8f87dea..29a829bcb0 100644 --- a/apps/cli/lib/import-export/export/tests/export-database.test.ts +++ b/apps/cli/lib/site-database/tests/dump.test.ts @@ -2,12 +2,14 @@ import path from 'path'; import { move } from 'fs-extra'; import { vi } from 'vitest'; import { runWpCliCommand, WpCliResponse } from 'cli/lib/run-wp-cli-command'; +import { dumpDatabase } from 'cli/lib/site-database/dump-load'; +import { replaceMysql8OnlyCollations } from 'cli/lib/sqlite-collations'; import { LoggerError } from 'cli/logger'; -import { exportDatabaseToFile, exportDatabaseToMultipleFiles } from '../export-database'; import type { SiteData } from 'cli/lib/cli-config/core'; vi.mock( 'fs-extra' ); vi.mock( 'cli/lib/run-wp-cli-command' ); +vi.mock( 'cli/lib/sqlite-collations' ); const site = { id: 'site-1', name: 'Test Site', path: '/test/site' } as SiteData; @@ -29,7 +31,7 @@ async function captureError( promise: Promise< unknown > ): Promise< LoggerError return error as LoggerError; } -describe( 'export-database', () => { +describe( 'dumpDatabase', () => { let consoleErrorSpy: ReturnType< typeof vi.spyOn >; beforeEach( () => { @@ -41,11 +43,27 @@ describe( 'export-database', () => { consoleErrorSpy.mockRestore(); } ); - describe( 'exportDatabaseToFile', () => { + it.each( [ false, true ] )( + 'normalizes collations before dumping (split tables: %s)', + async ( splitTables ) => { + mockWpCliResult( { stdout: '[]' } ); + if ( splitTables ) { + await dumpDatabase( site, '/dest', true ); + } else { + await dumpDatabase( site, '/dest/db.sql' ); + } + expect( replaceMysql8OnlyCollations ).toHaveBeenCalledWith( site.path ); + expect( vi.mocked( replaceMysql8OnlyCollations ).mock.invocationCallOrder[ 0 ] ).toBeLessThan( + vi.mocked( runWpCliCommand ).mock.invocationCallOrder[ 0 ] + ); + } + ); + + describe( 'single file', () => { it( 'moves the exported file to its destination on success', async () => { mockWpCliResult( {} ); - await exportDatabaseToFile( site, '/dest/db.sql' ); + await dumpDatabase( site, '/dest/db.sql' ); expect( move ).toHaveBeenCalledWith( expect.stringContaining( path.join( site.path, 'studio-backup-db-export' ) ), @@ -57,7 +75,7 @@ describe( 'export-database', () => { it( 'logs and includes the WP-CLI SQLite error when the export fails', async () => { mockWpCliResult( { exitCode: 1, stderr: 'Error: no such table: wp_options\n' } ); - const error = await captureError( exportDatabaseToFile( site, '/dest/db.sql' ) ); + const error = await captureError( dumpDatabase( site, '/dest/db.sql' ) ); expect( error.message ).toBe( 'Database export failed: Error: no such table: wp_options' ); expect( error.code ).toBe( 'database_export' ); @@ -72,7 +90,7 @@ describe( 'export-database', () => { const stderr = 'Deprecated: noise in /wp-cli/a.php\nError: no such table: wp_options'; mockWpCliResult( { exitCode: 1, stderr } ); - const error = await captureError( exportDatabaseToFile( site, '/dest/db.sql' ) ); + const error = await captureError( dumpDatabase( site, '/dest/db.sql' ) ); expect( error.message ).toBe( 'Database export failed: Error: no such table: wp_options' ); expect( consoleErrorSpy ).toHaveBeenCalledWith( 'Database export failed', stderr ); @@ -81,18 +99,18 @@ describe( 'export-database', () => { it( 'keeps the generic message when WP-CLI prints nothing to stderr', async () => { mockWpCliResult( { exitCode: 1 } ); - const error = await captureError( exportDatabaseToFile( site, '/dest/db.sql' ) ); + const error = await captureError( dumpDatabase( site, '/dest/db.sql' ) ); expect( error.message ).toBe( 'Database export failed' ); expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); } ); - describe( 'exportDatabaseToMultipleFiles', () => { + describe( 'separate tables', () => { it( 'logs and includes the WP-CLI SQLite error when listing tables fails', async () => { mockWpCliResult( { exitCode: 1, stderr: 'Error: database disk image is malformed' } ); - const error = await captureError( exportDatabaseToMultipleFiles( site, '/dest' ) ); + const error = await captureError( dumpDatabase( site, '/dest', true ) ); expect( error.message ).toBe( 'Database export failed: Error: database disk image is malformed' @@ -108,7 +126,7 @@ describe( 'export-database', () => { mockWpCliResult( {} ); mockWpCliResult( { exitCode: 1, stderr: 'Error: near "FROM": syntax error' } ); - const error = await captureError( exportDatabaseToMultipleFiles( site, '/dest' ) ); + const error = await captureError( dumpDatabase( site, '/dest', true ) ); expect( error.message ).toBe( 'Database export failed for table wp_posts: Error: near "FROM": syntax error' @@ -124,7 +142,7 @@ describe( 'export-database', () => { mockWpCliResult( { stdout: '["wp_options","wp_users","wp_usermeta"]' } ); mockWpCliResult( {} ); - const files = await exportDatabaseToMultipleFiles( site, '/dest' ); + const files = await dumpDatabase( site, '/dest', true ); expect( files ).toEqual( [ path.join( '/dest', 'wp_options.sql' ) ] ); expect( runWpCliCommand ).toHaveBeenCalledTimes( 2 ); diff --git a/apps/cli/lib/site-database/tests/load.test.ts b/apps/cli/lib/site-database/tests/load.test.ts new file mode 100644 index 0000000000..31c9100582 --- /dev/null +++ b/apps/cli/lib/site-database/tests/load.test.ts @@ -0,0 +1,101 @@ +import fs from 'fs'; +import path from 'path'; +import { DEFAULT_PHP_VERSION } from '@studio/common/constants'; +import { generateBackupFilename } from '@studio/common/lib/generate-backup-filename'; +import { move } from 'fs-extra'; +import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; +import { loadDatabase } from 'cli/lib/site-database/dump-load'; +import type { SiteData } from 'cli/lib/cli-config/core'; + +vi.mock( 'fs', () => ( { default: { promises: { rm: vi.fn() } } } ) ); +vi.mock( 'fs-extra' ); +vi.mock( 'cli/lib/run-wp-cli-command' ); +vi.mock( '@studio/common/lib/generate-backup-filename' ); + +const site = { path: '/site' } as SiteData; + +describe( 'loadDatabase', () => { + const prepareSqlFile = vi.fn().mockResolvedValue( undefined ); + const onProgress = vi.fn(); + const options = { phpVersion: DEFAULT_PHP_VERSION, prepareSqlFile, onProgress }; + + beforeEach( () => { + vi.clearAllMocks(); + vi.mocked( generateBackupFilename ).mockReturnValue( 'temporary' ); + vi.mocked( runWpCliCommand ).mockResolvedValue( { + response: { + exitCode: Promise.resolve( 0 ), + stderrText: Promise.resolve( '' ), + }, + [ Symbol.dispose ]: vi.fn(), + } as unknown as Awaited< ReturnType< typeof runWpCliCommand > > ); + } ); + + it( 'loads files in sorted order, preparing each temporary file and reporting progress', async () => { + await loadDatabase( site, [ '/backup/b.sql', '/backup/a.sql' ], options ); + expect( onProgress.mock.calls ).toEqual( [ + [ '/backup/a.sql', 1, 2 ], + [ '/backup/b.sql', 2, 2 ], + ] ); + const temporaryPath = path.join( site.path, 'temporary.sql' ); + expect( move ).toHaveBeenNthCalledWith( 1, '/backup/a.sql', temporaryPath ); + expect( prepareSqlFile ).toHaveBeenCalledWith( temporaryPath ); + expect( runWpCliCommand ).toHaveBeenCalledWith( + site, + [ + 'sqlite', + 'import', + 'temporary.sql', + '--enable-ast-driver', + '--skip-plugins', + '--skip-themes', + ], + { requireSqliteCliCommand: true, phpVersion: DEFAULT_PHP_VERSION } + ); + expect( fs.promises.rm ).toHaveBeenCalledTimes( 2 ); + expect( fs.promises.rm ).toHaveBeenCalledWith( temporaryPath, { + recursive: true, + force: true, + } ); + } ); + + it( 'does nothing for an empty file list', async () => { + await loadDatabase( site, [], options ); + expect( runWpCliCommand ).not.toHaveBeenCalled(); + expect( onProgress ).not.toHaveBeenCalled(); + } ); + + it( 'preserves import error output and cleans up after a failed command', async () => { + const stderr = 'Deprecated: noise\nError: invalid SQL'; + vi.mocked( runWpCliCommand ).mockResolvedValueOnce( { + response: { exitCode: Promise.resolve( 1 ), stderrText: Promise.resolve( stderr ) }, + [ Symbol.dispose ]: vi.fn(), + } as unknown as Awaited< ReturnType< typeof runWpCliCommand > > ); + const consoleError = vi.spyOn( console, 'error' ).mockImplementation( () => {} ); + try { + await expect( loadDatabase( site, [ '/backup/a.sql' ], options ) ).rejects.toMatchObject( { + message: 'Database import failed: Error: invalid SQL', + code: 'database_import', + } ); + expect( consoleError ).toHaveBeenCalledWith( + 'Error during import of /backup/a.sql:', + stderr + ); + expect( fs.promises.rm ).toHaveBeenCalled(); + } finally { + consoleError.mockRestore(); + } + } ); + + it( 'cleans up the temporary file when preparation fails', async () => { + prepareSqlFile.mockRejectedValueOnce( new Error( 'preparation failed' ) ); + await expect( loadDatabase( site, [ '/backup/a.sql' ], options ) ).rejects.toThrow( + 'preparation failed' + ); + expect( fs.promises.rm ).toHaveBeenCalledWith( path.join( site.path, 'temporary.sql' ), { + recursive: true, + force: true, + } ); + expect( runWpCliCommand ).not.toHaveBeenCalled(); + } ); +} ); diff --git a/apps/cli/lib/sqlite-collations.ts b/apps/cli/lib/sqlite-collations.ts index 120467683b..714f94e7dc 100644 --- a/apps/cli/lib/sqlite-collations.ts +++ b/apps/cli/lib/sqlite-collations.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import path from 'path'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; const MYSQL8_COLLATION_COLUMNS = [ { table: '_wp_sqlite_mysql_information_schema_schemata', column: 'DEFAULT_COLLATION_NAME' }, @@ -19,10 +19,6 @@ const MYSQL8_COLLATION_COLUMNS = [ * Collations are metadata only in SQLite, so rewriting them doesn't change * query behavior. */ -export function getSiteDatabasePath( sitePath: string ): string { - return path.join( sitePath, 'wp-content', 'database', '.ht.sqlite' ); -} - export async function replaceMysql8OnlyCollations( sitePath: string ): Promise< void > { const dbPath = getSiteDatabasePath( sitePath ); if ( ! fs.existsSync( dbPath ) ) { diff --git a/apps/cli/lib/sqlite-journal-mode.ts b/apps/cli/lib/sqlite-journal-mode.ts index 5862115f95..2ecb87de8b 100644 --- a/apps/cli/lib/sqlite-journal-mode.ts +++ b/apps/cli/lib/sqlite-journal-mode.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import path from 'path'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; /** * Switches a site's SQLite database out of WAL journal mode and into the @@ -22,7 +22,7 @@ import path from 'path'; * rewrite the header back to rollback mode. */ export async function resetSqliteJournalModeToRollback( sitePath: string ): Promise< void > { - const dbPath = path.join( sitePath, 'wp-content', 'database', '.ht.sqlite' ); + const dbPath = getSiteDatabasePath( sitePath ); if ( ! fs.existsSync( dbPath ) ) { return; } diff --git a/apps/cli/lib/static-site-import/prepare.ts b/apps/cli/lib/static-site-import/prepare.ts index e4f8f1e842..c6322122cf 100644 --- a/apps/cli/lib/static-site-import/prepare.ts +++ b/apps/cli/lib/static-site-import/prepare.ts @@ -12,7 +12,7 @@ import { type PartialCaptureReport, } from 'cli/lib/data-liberation-client'; import { resolveStaticSiteImporterPlugin } from 'cli/lib/import-runtime'; -import { isSqliteIntegrationAvailable } from 'cli/lib/sqlite-integration'; +import { isSqliteIntegrationAvailable } from 'cli/lib/site-database'; import { STATIC_SITE_IMPORT_PROGRESS_INTERVAL_MS, stagedUrlImport, diff --git a/apps/cli/lib/static-site-import/tests/prepare.test.ts b/apps/cli/lib/static-site-import/tests/prepare.test.ts index 44432a1e6e..af8e9c6412 100644 --- a/apps/cli/lib/static-site-import/tests/prepare.test.ts +++ b/apps/cli/lib/static-site-import/tests/prepare.test.ts @@ -2,11 +2,11 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { vi } from 'vitest'; -import { isSqliteIntegrationAvailable } from 'cli/lib/sqlite-integration'; +import { isSqliteIntegrationAvailable } from 'cli/lib/site-database'; import { prepareSourceImport } from 'cli/lib/static-site-import/prepare'; import { Logger } from 'cli/logger'; -vi.mock( 'cli/lib/sqlite-integration' ); +vi.mock( 'cli/lib/site-database' ); describe( 'prepareSourceImport', () => { beforeEach( () => { diff --git a/apps/cli/lib/tests/site-database.test.ts b/apps/cli/lib/tests/site-database.test.ts new file mode 100644 index 0000000000..b4fb58e167 --- /dev/null +++ b/apps/cli/lib/tests/site-database.test.ts @@ -0,0 +1,26 @@ +import { SqliteIntegrationProvider } from '@studio/common/lib/sqlite-integration'; +import { ensureSiteDatabaseReady } from 'cli/lib/site-database'; + +vi.mock( '@studio/common/lib/sqlite-integration' ); + +describe( 'ensureSiteDatabaseReady', () => { + beforeEach( () => { + vi.clearAllMocks(); + } ); + + it( 'prepares the database through the CLI integration provider', async () => { + await ensureSiteDatabaseReady( '/site' ); + expect( SqliteIntegrationProvider.prototype.ensureSiteDatabaseReady ).toHaveBeenCalledWith( + '/site' + ); + } ); + + it( 'propagates preparation failures to the caller', async () => { + vi.mocked( SqliteIntegrationProvider.prototype.ensureSiteDatabaseReady ).mockRejectedValueOnce( + new Error( 'SQLite integration files not found.' ) + ); + await expect( ensureSiteDatabaseReady( '/site' ) ).rejects.toThrow( + 'SQLite integration files not found.' + ); + } ); +} ); diff --git a/apps/cli/lib/tests/wordpress-server-manager.test.ts b/apps/cli/lib/tests/wordpress-server-manager.test.ts index 0eef7d3005..81ff5059c6 100644 --- a/apps/cli/lib/tests/wordpress-server-manager.test.ts +++ b/apps/cli/lib/tests/wordpress-server-manager.test.ts @@ -28,10 +28,12 @@ vi.mock( 'cli/lib/dependency-management/php-binary', () => ( { vi.mock( 'cli/lib/site-runtime-stats', () => ( { recordSiteRuntimeUsage: vi.fn(), } ) ); -vi.mock( 'cli/lib/sqlite-collations', () => ( { +vi.mock( '@studio/common/lib/site-database', () => ( { getSiteDatabasePath: vi.fn( ( sitePath: string ) => `${ sitePath }/wp-content/database/.ht.sqlite` ), +} ) ); +vi.mock( 'cli/lib/sqlite-collations', () => ( { replaceMysql8OnlyCollations: vi.fn().mockResolvedValue( undefined ), } ) ); vi.mock( 'cli/lib/sqlite-journal-mode', () => ( { diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index b3b68342ed..e677fa9061 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -14,6 +14,7 @@ import { import { readLastLines } from '@studio/common/lib/fs-utils'; import { STUDIO_ERROR_LOG_FILENAME } from '@studio/common/lib/mu-plugins'; import { resolveSupportedPhpVersion } from '@studio/common/lib/php-binary-metadata'; +import { getSiteDatabasePath } from '@studio/common/lib/site-database'; import { getWpEnvironmentType } from '@studio/common/lib/wp-environment-type'; import { SiteCommandLoggerAction } from '@studio/common/logger-actions'; import { __ } from '@wordpress/i18n'; @@ -31,7 +32,7 @@ import { } from 'cli/lib/daemon-client'; import { ensurePhpBinaryAvailable } from 'cli/lib/dependency-management/php-binary'; import { recordSiteRuntimeUsage } from 'cli/lib/site-runtime-stats'; -import { getSiteDatabasePath, replaceMysql8OnlyCollations } from 'cli/lib/sqlite-collations'; +import { replaceMysql8OnlyCollations } from 'cli/lib/sqlite-collations'; import { resetSqliteJournalModeToRollback } from 'cli/lib/sqlite-journal-mode'; import { getTracksOrigin, recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks'; import { ProcessDescription } from 'cli/lib/types/process-manager-ipc'; diff --git a/packages/common/lib/site-database.ts b/packages/common/lib/site-database.ts new file mode 100644 index 0000000000..36f92b0414 --- /dev/null +++ b/packages/common/lib/site-database.ts @@ -0,0 +1,26 @@ +import fs from 'fs'; +import path from 'path'; + +export type DatabaseEngine = 'sqlite' | 'mysql'; + +export function getSiteDatabasePath( sitePath: string ): string { + return path.join( sitePath, 'wp-content', 'database', '.ht.sqlite' ); +} + +// With no site selected, describe Studio's default engine. For an existing site, +// preserve external database configurations only when every SQLite artifact is absent. +export function getSiteDatabaseEngine( + sitePath?: string, + sqliteDirname = 'sqlite-database-integration' +): DatabaseEngine { + if ( sitePath === undefined ) { + return 'sqlite'; + } + const wpContentPath = path.join( sitePath, 'wp-content' ); + return ! fs.existsSync( path.join( sitePath, 'wp-config.php' ) ) || + fs.existsSync( path.join( wpContentPath, 'db.php' ) ) || + fs.existsSync( getSiteDatabasePath( sitePath ) ) || + fs.existsSync( path.join( wpContentPath, 'mu-plugins', sqliteDirname ) ) + ? 'sqlite' + : 'mysql'; +} diff --git a/packages/common/lib/sqlite-integration.ts b/packages/common/lib/sqlite-integration.ts index 21ccdbb6d1..1c750a40f2 100644 --- a/packages/common/lib/sqlite-integration.ts +++ b/packages/common/lib/sqlite-integration.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import { getSiteDatabaseEngine } from './site-database'; // Identifies a db.php that Studio generated from db.copy (see the auto-generated // header in that template). Stock drop-ins are refreshed on every run; custom, @@ -30,15 +31,7 @@ export abstract class SqliteIntegrationProvider { * is still present; skip only when wp-config.php exists and every SQLite artifact is gone. */ async needsSqliteSetup( sitePath: string ): Promise< boolean > { - const wpContentPath = path.join( sitePath, 'wp-content' ); - if ( ! fs.existsSync( path.join( sitePath, 'wp-config.php' ) ) ) { - return true; - } - return ( - fs.existsSync( path.join( wpContentPath, 'db.php' ) ) || - fs.existsSync( path.join( wpContentPath, 'database', '.ht.sqlite' ) ) || - fs.existsSync( path.join( wpContentPath, 'mu-plugins', this.getSqliteDirname() ) ) - ); + return getSiteDatabaseEngine( sitePath, this.getSqliteDirname() ) === 'sqlite'; } /** @@ -115,7 +108,7 @@ export abstract class SqliteIntegrationProvider { } ); } - async keepSqliteIntegrationUpdated( sitePath: string ): Promise< void > { + async ensureSiteDatabaseReady( sitePath: string ): Promise< void > { if ( await this.needsSqliteSetup( sitePath ) ) { await this.installSqliteIntegration( sitePath ); } diff --git a/packages/common/lib/tests/site-database.test.ts b/packages/common/lib/tests/site-database.test.ts new file mode 100644 index 0000000000..70b48da44a --- /dev/null +++ b/packages/common/lib/tests/site-database.test.ts @@ -0,0 +1,52 @@ +import path from 'path'; +import { vol } from 'memfs'; +import { getSiteDatabaseEngine, getSiteDatabasePath } from '../site-database'; + +vi.mock( 'fs' ); + +describe( 'site database', () => { + const sitePath = '/site'; + + beforeEach( () => { + vol.reset(); + } ); + + it( 'keeps the current database location for absolute and relative site paths', () => { + expect( getSiteDatabasePath( sitePath ) ).toBe( + path.join( sitePath, 'wp-content', 'database', '.ht.sqlite' ) + ); + expect( getSiteDatabasePath( 'site' ) ).toBe( + path.join( 'site', 'wp-content', 'database', '.ht.sqlite' ) + ); + } ); + + it( 'defaults fresh sites and descriptions without a selected site to SQLite', () => { + expect( getSiteDatabaseEngine() ).toBe( 'sqlite' ); + expect( getSiteDatabaseEngine( sitePath ) ).toBe( 'sqlite' ); + } ); + + it( 'preserves an external database configuration with no SQLite artifacts', () => { + vol.fromJSON( { '/site/wp-config.php': ' { + vol.fromJSON( { + '/site/wp-config.php': ' { + vol.fromJSON( { + '/site/wp-config.php': ' { } ); } ); - describe( 'keepSqliteIntegrationUpdated', () => { + describe( 'ensureSiteDatabaseReady', () => { beforeEach( () => { volFromJSON( { [ normalize( `wp-files/${ SQLITE_DIRNAME }/db.copy` ) ]: @@ -255,7 +255,7 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { it( 'should install when db.php exists', async () => { volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: '' } ); - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); + await provider.ensureSiteDatabaseReady( MOCK_SITE_PATH ); expect( vi.mocked( fs.promises.cp ) ).toHaveBeenCalledWith( normalize( `wp-files/${ SQLITE_DIRNAME }` ), @@ -265,7 +265,7 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { } ); it( 'should install when wp-config.php does not exist', async () => { - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); + await provider.ensureSiteDatabaseReady( MOCK_SITE_PATH ); expect( vi.mocked( fs.promises.cp ) ).toHaveBeenCalledWith( normalize( `wp-files/${ SQLITE_DIRNAME }` ), @@ -282,7 +282,7 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { [ normalize( `${ MOCK_SITE_PATH }/wp-content/database/.ht.sqlite` ) ]: '', } ); - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); + await provider.ensureSiteDatabaseReady( MOCK_SITE_PATH ); expect( vi.mocked( fs.promises.writeFile ) ).toHaveBeenCalledWith( normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ), @@ -301,7 +301,7 @@ platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { // at MySQL. Studio must not reinstall the SQLite integration over that. volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-config.php` ) ]: 'config' } ); - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); + await provider.ensureSiteDatabaseReady( MOCK_SITE_PATH ); expect( vi.mocked( fs.promises.writeFile ) ).not.toHaveBeenCalled(); expect( vi.mocked( fs.promises.cp ) ).not.toHaveBeenCalled();