Skip to content

Commit 42fde81

Browse files
committed
Test SQLite native parser extension assets
1 parent 8a0080e commit 42fde81

14 files changed

Lines changed: 333 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,30 @@ jobs:
394394
node-version: 20
395395
- run: packages/php-wasm/cli/tests/smoke-test.sh
396396

397+
test-wp-mysql-parser-extension:
398+
if: github.repository == 'WordPress/wordpress-playground' || github.event_name == 'pull_request'
399+
runs-on: ubuntu-latest
400+
strategy:
401+
fail-fast: false
402+
matrix:
403+
include:
404+
- async-mode: asyncify
405+
node-version: 20
406+
target: test-wp-mysql-parser-extension-asyncify
407+
- async-mode: jspi
408+
node-version: 24
409+
target: test-wp-mysql-parser-extension-jspi
410+
name: 'test-wp-mysql-parser-extension-${{ matrix.async-mode }}'
411+
steps:
412+
- uses: actions/checkout@v4
413+
with:
414+
submodules: true
415+
- uses: ./.github/actions/prepare-playground
416+
with:
417+
node-version: ${{ matrix.node-version }}
418+
- name: Run native parser extension workload
419+
run: node --expose-gc node_modules/nx/bin/nx run php-wasm-node:${{ matrix.target }}
420+
397421
test-legacy-wp-version-boot:
398422
if: github.repository == 'WordPress/wordpress-playground' || github.event_name == 'pull_request'
399423
runs-on: ubuntu-latest

packages/php-wasm/node-builds/8-4/asyncify/php_8_4.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/php-wasm/node-builds/8-4/jspi/php_8_4.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/php-wasm/node/project.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,23 @@
344344
"testNamePattern": "Memcached Network Integration"
345345
}
346346
},
347+
"test-wp-mysql-parser-extension-asyncify": {
348+
"executor": "@nx/vitest:test",
349+
"outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"],
350+
"options": {
351+
"reportsDirectory": "../../../coverage/packages/php-wasm/node",
352+
"testFiles": ["wp-mysql-parser-extension.spec.ts"]
353+
}
354+
},
355+
"test-wp-mysql-parser-extension-jspi": {
356+
"executor": "@nx/vitest:test",
357+
"outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"],
358+
"options": {
359+
"configFile": "packages/php-wasm/node/vite.jspi.config.ts",
360+
"reportsDirectory": "../../../coverage/packages/php-wasm/node",
361+
"testFiles": ["wp-mysql-parser-extension.spec.ts"]
362+
}
363+
},
347364
"test-file-locking-asyncify": {
348365
"executor": "@nx/vite:test",
349366
"outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"],
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { existsSync, readFileSync } from 'fs';
2+
import { readFile } from 'fs/promises';
3+
import { fileURLToPath } from 'url';
4+
import { PHP } from '@php-wasm/universal';
5+
import { unzipFile } from '@wp-playground/common';
6+
import { loadNodeRuntime, type PHPLoaderExtension } from '../lib';
7+
8+
const parserExtensionManifestUrl = new URL(
9+
'../../../../playground/wordpress-builds/src/sqlite-database-integration/wp-mysql-parser/manifest.json',
10+
import.meta.url
11+
);
12+
const sqliteIntegrationZipUrl = new URL(
13+
'../../../../playground/wordpress-builds/src/sqlite-database-integration/sqlite-database-integration-pr388.zip',
14+
import.meta.url
15+
);
16+
17+
describe('WP MySQL parser PHP.wasm extension', () => {
18+
let phpInstances: PHP[] = [];
19+
20+
afterEach(() => {
21+
for (const php of phpInstances) {
22+
php.exit();
23+
}
24+
phpInstances = [];
25+
});
26+
27+
it('loads the PR388 native parser extension through the runtime manifest and processes 5000 long queries', async () => {
28+
expect(existsSync(fileURLToPath(parserExtensionManifestUrl))).toBe(
29+
true
30+
);
31+
expect(existsSync(fileURLToPath(sqliteIntegrationZipUrl))).toBe(true);
32+
33+
const fallback = await runParserBenchmark({
34+
loadNativeExtension: false,
35+
});
36+
const native = await runParserBenchmark({
37+
loadNativeExtension: true,
38+
});
39+
40+
expect(fallback).toMatchObject({
41+
extensionLoaded: false,
42+
nativeLexerUsed: false,
43+
processedQueries: 5000,
44+
});
45+
expect(native).toMatchObject({
46+
extensionLoaded: true,
47+
nativeLexerUsed: true,
48+
processedQueries: 5000,
49+
});
50+
expect(native.totalTokens).toBeGreaterThan(0);
51+
expect(fallback.totalTokens).toBeGreaterThan(0);
52+
expect(native.durationMs).toBeLessThan(fallback.durationMs);
53+
}, 180_000);
54+
55+
async function runParserBenchmark({
56+
loadNativeExtension,
57+
}: {
58+
loadNativeExtension: boolean;
59+
}): Promise<ParserBenchmarkResult> {
60+
const extensions: PHPLoaderExtension[] = loadNativeExtension
61+
? [
62+
{
63+
source: {
64+
format: 'manifest',
65+
url: parserExtensionManifestUrl,
66+
},
67+
fetch: fetchLocalFile,
68+
},
69+
]
70+
: [];
71+
const php = new PHP(
72+
await loadNodeRuntime('8.4', {
73+
extensions,
74+
})
75+
);
76+
phpInstances.push(php);
77+
78+
await unzipFile(
79+
php,
80+
new File(
81+
[readFileSync(fileURLToPath(sqliteIntegrationZipUrl)) as any],
82+
'sqlite-database-integration-pr388.zip'
83+
),
84+
'/tmp/sqlite-driver'
85+
);
86+
87+
const result = await php.run({
88+
code: parserBenchmarkPhp,
89+
});
90+
91+
expect(result.errors).toBeFalsy();
92+
return JSON.parse(result.text) as ParserBenchmarkResult;
93+
}
94+
});
95+
96+
interface ParserBenchmarkResult {
97+
extensionLoaded: boolean;
98+
nativeLexerUsed: boolean;
99+
processedQueries: number;
100+
totalTokens: number;
101+
durationMs: number;
102+
}
103+
104+
const parserBenchmarkPhp = `<?php
105+
$lexer_class_path = '/tmp/sqlite-driver/plugin-sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-lexer.php';
106+
if (extension_loaded('wp_mysql_parser')) {
107+
file_put_contents($lexer_class_path, <<<'PHP'
108+
<?php
109+
110+
require_once __DIR__ . '/class-wp-mysql-polyfill-lexer.php';
111+
112+
if (class_exists('WP_MySQL_Native_Lexer', false)) {
113+
class WP_MySQL_Lexer extends WP_MySQL_Polyfill_Lexer {
114+
private $native_lexer;
115+
116+
public function __construct(
117+
string $sql,
118+
int $mysql_version = 80038,
119+
array $sql_modes = array()
120+
) {
121+
parent::__construct($sql, $mysql_version, $sql_modes);
122+
$this->native_lexer = new WP_MySQL_Native_Lexer(
123+
$sql,
124+
$mysql_version,
125+
$sql_modes
126+
);
127+
}
128+
129+
public function native_token_stream() {
130+
return $this->native_lexer->native_token_stream();
131+
}
132+
}
133+
} else {
134+
require_once __DIR__ . '/class-wp-mysql-native-lexer.php';
135+
136+
class WP_MySQL_Lexer extends WP_MySQL_Native_Lexer {
137+
}
138+
}
139+
PHP);
140+
}
141+
142+
require_once '/tmp/sqlite-driver/plugin-sqlite-database-integration/wp-includes/database/load.php';
143+
144+
$driver = new WP_PDO_MySQL_On_SQLite(
145+
'mysql-on-sqlite:dbname=playground_native_parser_benchmark;path=:memory:'
146+
);
147+
$query_count = 5000;
148+
$processed_queries = 0;
149+
$total_tokens = 0;
150+
$started_at = hrtime(true);
151+
152+
for ($i = 0; $i < $query_count; $i++) {
153+
$needle = 'native-parser-' . $i;
154+
$offset = $i % 100;
155+
$query = "SELECT p.ID, p.post_title, p.post_name, u.display_name, " .
156+
"mt1.meta_value AS color, mt2.meta_value AS size " .
157+
"FROM wp_posts p " .
158+
"LEFT JOIN wp_postmeta mt1 ON (p.ID = mt1.post_id AND mt1.meta_key = '_color') " .
159+
"LEFT JOIN wp_postmeta mt2 ON (p.ID = mt2.post_id AND mt2.meta_key = '_size') " .
160+
"INNER JOIN wp_users u ON (p.post_author = u.ID) " .
161+
"WHERE p.post_type IN ('post', 'page', 'attachment') " .
162+
"AND p.post_status NOT IN ('trash', 'auto-draft') " .
163+
"AND (p.post_title LIKE '%" . $needle . "%' " .
164+
"OR p.post_content LIKE '%" . $needle . "%' " .
165+
"OR mt1.meta_value = 'blue') " .
166+
"GROUP BY p.ID, p.post_title, p.post_name, u.display_name, mt1.meta_value, mt2.meta_value " .
167+
"ORDER BY p.post_date DESC, p.ID ASC " .
168+
"LIMIT " . $offset . ", 20";
169+
170+
if (extension_loaded('wp_mysql_parser')) {
171+
$total_tokens += (new WP_MySQL_Lexer($query))->native_token_stream()->count();
172+
} else {
173+
$tokens = (new WP_MySQL_Lexer($query))->remaining_tokens();
174+
if (!$tokens) {
175+
throw new Exception('Lexer returned no tokens at query ' . $i);
176+
}
177+
$total_tokens += count($tokens);
178+
}
179+
$processed_queries++;
180+
}
181+
182+
echo json_encode(array(
183+
'extensionLoaded' => extension_loaded('wp_mysql_parser'),
184+
'nativeLexerUsed' => method_exists('WP_MySQL_Lexer', 'native_token_stream'),
185+
'processedQueries' => $processed_queries,
186+
'totalTokens' => $total_tokens,
187+
'durationMs' => (hrtime(true) - $started_at) / 1000000,
188+
));
189+
`;
190+
191+
async function fetchLocalFile(input: RequestInfo | URL): Promise<Response> {
192+
const url =
193+
input instanceof Request ? new URL(input.url) : new URL(String(input));
194+
if (url.protocol !== 'file:') {
195+
return fetch(input);
196+
}
197+
198+
try {
199+
return new Response(await readFile(fileURLToPath(url)), {
200+
status: 200,
201+
});
202+
} catch (error) {
203+
return new Response(String(error), {
204+
status: 404,
205+
statusText: 'Not Found',
206+
});
207+
}
208+
}

packages/php-wasm/web-builds/8-4/asyncify/php_8_4.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/php-wasm/web-builds/8-4/jspi/php_8_4.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import url_trunk from './sqlite-database-integration-trunk.zip?url';
44
import url_v2_1_16 from './sqlite-database-integration-v2.1.16.zip?url';
55
// @ts-ignore
66
import url_v3_0_0_rc_3_php52 from './sqlite-database-integration-v3.0.0-rc.3-php52.zip?url';
7+
// @ts-ignore
8+
import url_pr388 from './sqlite-database-integration-pr388.zip?url';
79

810
/**
911
* This file was auto generated by:
@@ -39,6 +41,12 @@ export function getSqliteDriverModuleDetails(
3941
size: 210820,
4042
url: url_v3_0_0_rc_3_php52,
4143
};
44+
case 'pr388':
45+
/** @ts-ignore */
46+
return {
47+
size: 214682,
48+
url: url_pr388,
49+
};
4250
}
4351
throw new Error(
4452
'Unsupported SQLite integration plugin version: ' + version
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2-
"trunk": "trunk",
3-
"v2.1.16": "v2.1.16"
4-
}
2+
"trunk": "trunk",
3+
"v2.1.16": "v2.1.16",
4+
"pr388": "pr388"
5+
}

0 commit comments

Comments
 (0)