Skip to content

Commit 094b085

Browse files
committed
Resolve active SQLite paths in database panels
Use WordPress environment metadata for database paths, sizes, and downloads in Playground and Personal WP. Share the path lookup between both panels. Register the metadata writer from a common preload and save at PHP shutdown so a failing site MU plugin does not prevent database recovery. Support legacy WordPress through the same writer, and ignore requests without a WordPress database object. Verify active custom paths, failed-boot recovery, and legacy metadata. Related: WordPress/sqlite-database-integration#502
1 parent 563921e commit 094b085

9 files changed

Lines changed: 210 additions & 70 deletions

File tree

‎packages/playground/personal-wp/src/components/site-manager/site-database-panel/download-button.tsx‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { Button, Icon, Flex, FlexItem } from '@wordpress/components';
22
import { download } from '@wordpress/icons';
33
import type { PlaygroundClient } from '@wp-playground/client';
44

5-
const DATABASE_PATH = '/wordpress/wp-content/database/.ht.sqlite';
6-
7-
async function downloadDatabase(playground: PlaygroundClient): Promise<void> {
8-
const fileExists = await playground.fileExists(DATABASE_PATH);
5+
async function downloadDatabase(
6+
playground: PlaygroundClient,
7+
databasePath: string
8+
): Promise<void> {
9+
const fileExists = await playground.fileExists(databasePath);
910
if (!fileExists) {
1011
throw new Error('Database file does not exist');
1112
}
1213

13-
const buffer = await playground.readFileAsBuffer(DATABASE_PATH);
14+
const buffer = await playground.readFileAsBuffer(databasePath);
1415
const blob = new Blob([new Uint8Array(buffer)], {
1516
type: 'application/x-sqlite3',
1617
});
@@ -25,15 +26,19 @@ async function downloadDatabase(playground: PlaygroundClient): Promise<void> {
2526

2627
export function DownloadButton({
2728
playground,
29+
databasePath,
2830
}: {
2931
playground: PlaygroundClient | undefined;
32+
databasePath: string | null;
3033
}) {
3134
return (
3235
<Button
3336
variant="secondary"
34-
disabled={!playground}
37+
disabled={!playground || !databasePath}
3538
onClick={
36-
playground ? () => downloadDatabase(playground) : undefined
39+
playground && databasePath
40+
? () => downloadDatabase(playground, databasePath)
41+
: undefined
3742
}
3843
>
3944
<Flex justify="space-between" gap={2} expanded={true}>

‎packages/playground/personal-wp/src/components/site-manager/site-database-panel/index.tsx‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,52 @@
11
import { useState, useEffect } from 'react';
22
import type { PlaygroundClient } from '@wp-playground/client';
3+
import { getSqliteDatabasePath } from '@wp-playground/tools';
34
import { Notice, __experimentalVStack as VStack } from '@wordpress/components';
45
import { DownloadButton } from './download-button';
56
import { AdminerButton } from './adminer-button';
67
import { PhpMyAdminButton } from './phpmyadmin-button';
78
import css from './style.module.css';
89
import { formatBytes } from '../../../lib/utils/format-bytes';
910

10-
const DATABASE_PATH = '/wordpress/wp-content/database/.ht.sqlite';
11-
1211
export function SiteDatabasePanel({
1312
playground,
1413
}: {
1514
playground: PlaygroundClient | undefined;
1615
}) {
16+
const [databasePath, setDatabasePath] = useState<string | null>(null);
1717
const [databaseSize, setDatabaseSize] = useState<number | null>(null);
1818

1919
useEffect(() => {
20+
setDatabasePath(null);
21+
setDatabaseSize(null);
2022
if (!playground) {
21-
setDatabaseSize(null);
2223
return;
2324
}
2425

26+
let cancelled = false;
27+
2528
async function fetchDatabaseSize() {
2629
if (!playground) return;
2730

2831
try {
29-
const fileExists = await playground.fileExists(DATABASE_PATH);
32+
const path = await getSqliteDatabasePath(playground);
33+
if (cancelled) return;
34+
setDatabasePath(path);
35+
const fileExists = await playground.fileExists(path);
3036
if (fileExists) {
31-
const buffer =
32-
await playground.readFileAsBuffer(DATABASE_PATH);
37+
const buffer = await playground.readFileAsBuffer(path);
38+
if (cancelled) return;
3339
setDatabaseSize(buffer.byteLength);
34-
} else {
35-
setDatabaseSize(null);
3640
}
3741
} catch {
38-
setDatabaseSize(null);
42+
if (!cancelled) setDatabaseSize(null);
3943
}
4044
}
4145

4246
void fetchDatabaseSize();
47+
return () => {
48+
cancelled = true;
49+
};
4350
}, [playground]);
4451

4552
return (
@@ -83,7 +90,7 @@ export function SiteDatabasePanel({
8390
</span>
8491
<span className={css.label}>SQLite database path:</span>
8592
<span className={css.value}>
86-
<code>{DATABASE_PATH}</code>
93+
<code>{databasePath ?? 'Unavailable'}</code>
8794
</span>
8895
{databaseSize !== null && (
8996
<>
@@ -97,7 +104,10 @@ export function SiteDatabasePanel({
97104
</VStack>
98105

99106
<div className={css.buttonGroup}>
100-
<DownloadButton playground={playground} />
107+
<DownloadButton
108+
playground={playground}
109+
databasePath={databasePath}
110+
/>
101111
<AdminerButton playground={playground} />
102112
<PhpMyAdminButton playground={playground} />
103113
</div>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { loadNodeRuntime } from '@php-wasm/node';
2+
import { RecommendedPHPVersion } from '@wp-playground/common';
3+
import {
4+
bootRequestHandler,
5+
bootWordPress,
6+
bootWordPressAndRequestHandler,
7+
} from '@wp-playground/wordpress';
8+
import { dirname } from '@php-wasm/util';
9+
import {
10+
getSqliteDriverModule,
11+
getWordPressModule,
12+
} from '@wp-playground/wordpress-builds';
13+
import { getSqliteDatabasePath } from './get-sqlite-database-path';
14+
15+
describe('getSqliteDatabasePath', () => {
16+
it('reads the active database from WordPress environment metadata', async () => {
17+
await using handler = await bootWordPressAndRequestHandler({
18+
createPhpRuntime: () => loadNodeRuntime(RecommendedPHPVersion),
19+
siteUrl: 'http://playground.test',
20+
wordPressZip: await getWordPressModule(),
21+
sqliteIntegrationPluginZip: await getSqliteDriverModule(),
22+
documentRoot: '/custom-root',
23+
dataSqlPath: '/custom/database.sqlite',
24+
});
25+
const php = await handler.getPrimaryPhp();
26+
const path = await getSqliteDatabasePath(php);
27+
expect(php.isFile(path)).toBe(true);
28+
expect(path).toBe('/custom/database.sqlite');
29+
await php.run({
30+
code: `<?php
31+
require getenv('DOCUMENT_ROOT') . '/wp-load.php';
32+
update_option('playground_database_path_test', 'active database');
33+
`,
34+
env: { DOCUMENT_ROOT: php.documentRoot },
35+
});
36+
const response = await php.run({
37+
code: `<?php
38+
$pdo = new PDO('sqlite:' . getenv('DATABASE_PATH'));
39+
echo $pdo->query("SELECT option_value FROM wp_options WHERE option_name = 'playground_database_path_test'")->fetchColumn();
40+
`,
41+
env: { DATABASE_PATH: path },
42+
});
43+
expect(response.errors).toBe('');
44+
expect(response.text).toBe('active database');
45+
}, 30_000);
46+
47+
it.each([undefined, '/custom/database.sqlite'])(
48+
'keeps the database accessible after a failed boot with dataSqlPath=%s',
49+
async (dataSqlPath) => {
50+
const options = {
51+
createPhpRuntime: () => loadNodeRuntime(RecommendedPHPVersion),
52+
siteUrl: 'http://playground.test',
53+
wordPressZip: await getWordPressModule(),
54+
sqliteIntegrationPluginZip: await getSqliteDriverModule(),
55+
dataSqlPath,
56+
};
57+
await using saved = await bootWordPressAndRequestHandler(options);
58+
const source = await saved.getPrimaryPhp();
59+
const path = await getSqliteDatabasePath(source);
60+
const database = source.readFileAsBuffer(path);
61+
await using reopened = await bootRequestHandler(options);
62+
await expect(
63+
bootWordPress(reopened, {
64+
...options,
65+
wordpressInstallMode:
66+
'install-from-existing-files-if-needed',
67+
hooks: {
68+
beforeDatabaseSetup: async (php) => {
69+
php.mkdir(dirname(path));
70+
php.writeFile(path, database);
71+
php.mkdir('/wordpress/wp-content/mu-plugins');
72+
php.writeFile(
73+
'/wordpress/wp-content/mu-plugins/broken.php',
74+
'<?php throw new Exception("A saved plugin failed");'
75+
);
76+
},
77+
},
78+
})
79+
).rejects.toThrow('A saved plugin failed');
80+
81+
const php = await reopened.getPrimaryPhp();
82+
expect(await getSqliteDatabasePath(php)).toBe(path);
83+
expect(php.readFileAsBuffer(path).byteLength).toBeGreaterThan(0);
84+
85+
// An unrelated PHP request must not overwrite the saved database metadata.
86+
await php.run({ code: '<?php echo "No WordPress loaded";' });
87+
expect(await getSqliteDatabasePath(php)).toBe(path);
88+
},
89+
30_000
90+
);
91+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { UniversalPHP } from '@php-wasm/universal';
2+
3+
export async function getSqliteDatabasePath(
4+
php: UniversalPHP
5+
): Promise<string> {
6+
const response = await php.run({
7+
code: `<?php
8+
$wp_env = require '/internal/shared/wp-env.php';
9+
if ($wp_env['db']['type'] !== 'sqlite') {
10+
throw new RuntimeException('The site does not use SQLite.');
11+
}
12+
echo json_encode($wp_env['db']['path']);
13+
`,
14+
});
15+
const path = response.json;
16+
if (typeof path !== 'string' || path === '') {
17+
throw new Error('The SQLite database path is invalid.');
18+
}
19+
return path;
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './phpmyadmin';
2+
export { getSqliteDatabasePath } from './get-sqlite-database-path';

‎packages/playground/website/playwright/e2e/website-ui.spec.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ test('should stat the database size without reading the database into JavaScript
10481048
const originalRead = playground.readFileAsBuffer.bind(playground);
10491049
(window as any).__databaseReadCount = 0;
10501050
playground.readFileAsBuffer = async (path: string) => {
1051-
if (path.endsWith('/wp-content/database/.ht.sqlite')) {
1051+
if (path.endsWith('/.ht.sqlite')) {
10521052
(window as any).__databaseReadCount++;
10531053
throw new Error(
10541054
'Database contents must not be read to calculate size.'
@@ -1075,9 +1075,19 @@ test.describe('Database panel', () => {
10751075
});
10761076

10771077
test('should display database info', async ({ website }) => {
1078+
const databasePath = await website.page.evaluate(async () => {
1079+
const playground = (window as any).playgroundSites.getClient();
1080+
const response = await playground.run({
1081+
code: `<?php
1082+
$wp_env = require '/internal/shared/wp-env.php';
1083+
echo $wp_env['db']['path'];
1084+
`,
1085+
});
1086+
return response.text;
1087+
});
10781088
await expect(website.page.getByText('Path:')).toBeVisible();
10791089
await expect(
1080-
website.page.getByText('/wordpress/wp-content/database/.ht.sqlite')
1090+
website.page.getByText(databasePath, { exact: true })
10811091
).toBeVisible();
10821092
await expect(website.page.getByText('Size:')).toBeVisible();
10831093
});

‎packages/playground/website/src/components/site-manager/site-database-panel/index.tsx‎

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect } from 'react';
2-
import { joinPaths } from '@php-wasm/util';
2+
import { getSqliteDatabasePath } from '@wp-playground/tools';
33
import type { PlaygroundClient } from '@wp-playground/client';
44
import { Notice } from '@wordpress/components';
55
import { PlaygroundBootNotice } from '../../pane-loading';
@@ -8,78 +8,43 @@ import { DownloadButton } from './download-button';
88
import { PhpMyAdminButton } from './phpmyadmin-button';
99
import css from './style.module.css';
1010

11-
const RELATIVE_DATABASE_PATH = 'wp-content/database/.ht.sqlite';
12-
1311
export function SiteDatabasePanel({
1412
playground,
1513
}: {
1614
playground: PlaygroundClient | undefined;
1715
}) {
18-
const [documentRoot, setDocumentRoot] = useState<string | null>(null);
16+
const [databasePath, setDatabasePath] = useState<string | null>(null);
1917
const [databaseSize, setDatabaseSize] = useState<number | null>(null);
2018
const [sizeStatus, setSizeStatus] = useState<
2119
'loading' | 'ready' | 'unavailable'
2220
>('loading');
2321

24-
// Resolve the real document root instead of assuming /wordpress, which is
25-
// wrong for Playgrounds mounted at a different root.
2622
useEffect(() => {
27-
if (!playground) {
28-
setDocumentRoot(null);
29-
setDatabaseSize(null);
30-
return;
31-
}
32-
setDocumentRoot(null);
23+
setDatabasePath(null);
3324
setDatabaseSize(null);
34-
let cancelled = false;
35-
void playground.documentRoot.then((root) => {
36-
if (!cancelled) {
37-
setDocumentRoot(root);
38-
}
39-
});
40-
return () => {
41-
cancelled = true;
42-
};
43-
}, [playground]);
44-
45-
const databasePath = documentRoot
46-
? joinPaths(documentRoot, RELATIVE_DATABASE_PATH)
47-
: null;
48-
49-
useEffect(() => {
5025
if (!playground) {
51-
// No client to inspect — don't sit on "Calculating…" forever.
5226
setSizeStatus('unavailable');
5327
return;
5428
}
55-
if (!databasePath) {
56-
// Client present but the document root is still resolving.
57-
setSizeStatus('loading');
58-
return;
59-
}
6029
let cancelled = false;
6130
setSizeStatus('loading');
6231

6332
async function fetchDatabaseSize() {
64-
if (!playground || !databasePath) return;
33+
if (!playground) return;
6534
try {
66-
const fileExists = await playground.fileExists(databasePath);
35+
const path = await getSqliteDatabasePath(playground);
6736
if (cancelled) return;
68-
if (fileExists) {
69-
const size = await readDatabaseSize(
70-
playground,
71-
databasePath
72-
);
37+
setDatabasePath(path);
38+
if (await playground.fileExists(path)) {
39+
const size = await readDatabaseSize(playground, path);
7340
if (cancelled) return;
7441
setDatabaseSize(size);
7542
setSizeStatus('ready');
76-
} else {
77-
setDatabaseSize(null);
43+
} else if (!cancelled) {
7844
setSizeStatus('unavailable');
7945
}
8046
} catch {
8147
if (!cancelled) {
82-
setDatabaseSize(null);
8348
setSizeStatus('unavailable');
8449
}
8550
}
@@ -89,7 +54,7 @@ export function SiteDatabasePanel({
8954
return () => {
9055
cancelled = true;
9156
};
92-
}, [playground, databasePath]);
57+
}, [playground]);
9358

9459
return (
9560
<div className={css.databasePanel}>
@@ -98,7 +63,12 @@ export function SiteDatabasePanel({
9863
<dd className={css.value}>MySQL emulation backed by SQLite</dd>
9964
<dt className={css.label}>Path:</dt>
10065
<dd className={css.value}>
101-
<code>{databasePath ?? `…/${RELATIVE_DATABASE_PATH}`}</code>
66+
<code>
67+
{databasePath ??
68+
(sizeStatus === 'loading'
69+
? 'Loading…'
70+
: 'Unavailable')}
71+
</code>
10272
</dd>
10373
<dt className={css.label}>Size:</dt>
10474
<dd className={css.value}>

0 commit comments

Comments
 (0)