Skip to content

Commit 47583f1

Browse files
committed
Add native SQLite facade smoke test
1 parent e8a02a9 commit 47583f1

4 files changed

Lines changed: 296 additions & 0 deletions

File tree

tmp-test-native/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Native SQLite Facade Smoke Test
2+
3+
This directory contains a local smoke test for the lazy native MySQL parser path
4+
as used by the SQLite driver.
5+
6+
Run the smoke test directly from the repository root:
7+
8+
```bash
9+
php tmp-test-native/run-sqlite-facade-smoke.php
10+
```
11+
12+
That file does not know or care whether the Rust extension is loaded. It just
13+
loads the SQLite library, runs MySQL-flavored queries through
14+
`WP_PDO_MySQL_On_SQLite`, asks the driver to create parsers, and traverses the
15+
returned ASTs. The library chooses the native parser/tokenizer when available
16+
and falls back to the PHP implementation otherwise.
17+
18+
By default, it processes 2000 generated SQL queries. That includes a
19+
2000-row multi-insert every 250 queries. To change the query count:
20+
21+
```bash
22+
TMP_TEST_NATIVE_QUERY_COUNT=500 php tmp-test-native/run-sqlite-facade-smoke.php
23+
```
24+
25+
To build the Rust extension locally and run the smoke test once with the current
26+
PHP configuration and once with the extension explicitly loaded:
27+
28+
```bash
29+
./tmp-test-native/run.sh
30+
```
31+
32+
If you already have a PHP/ext build environment and do not want the helper to
33+
enter a Nix shell, run:
34+
35+
```bash
36+
TMP_TEST_NATIVE_NO_NIX=1 ./tmp-test-native/run.sh
37+
```

tmp-test-native/build-extension.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
EXT_DIR="$ROOT_DIR/packages/mysql-on-sqlite/ext/wp-mysql-parser"
7+
8+
if [ ! -f "$EXT_DIR/Cargo.toml" ]; then
9+
echo "Cannot find Rust extension at $EXT_DIR" >&2
10+
exit 1
11+
fi
12+
13+
if [ "${1:-}" != "--inside-nix" ] && [ "${TMP_TEST_NATIVE_NO_NIX:-}" != "1" ] && command -v nix >/dev/null 2>&1; then
14+
exec nix --extra-experimental-features 'nix-command flakes' shell \
15+
nixpkgs#php82 \
16+
nixpkgs#php82.unwrapped.dev \
17+
nixpkgs#clang_19 \
18+
nixpkgs#glibc.dev \
19+
-c bash "$SCRIPT_DIR/build-extension.sh" --inside-nix
20+
fi
21+
22+
cd "$EXT_DIR"
23+
24+
PHP_CONFIG="${PHP_CONFIG:-$(command -v php-config || true)}"
25+
if [ -z "$PHP_CONFIG" ]; then
26+
echo "php-config is required. Use Nix or install the PHP development package." >&2
27+
exit 1
28+
fi
29+
30+
if ! command -v clang >/dev/null 2>&1; then
31+
echo "clang is required. Use Nix or install clang/libclang." >&2
32+
exit 1
33+
fi
34+
35+
if [ -z "${LIBCLANG_PATH:-}" ]; then
36+
LIBCLANG_SO="$(find /nix/store -path '*/lib/libclang.so' -print -quit 2>/dev/null || true)"
37+
if [ -n "$LIBCLANG_SO" ]; then
38+
export LIBCLANG_PATH
39+
LIBCLANG_PATH="$(dirname "$LIBCLANG_SO")"
40+
fi
41+
fi
42+
43+
PHP_INCLUDES="$("$PHP_CONFIG" --includes)"
44+
CLANG_RESOURCE="$(clang -print-resource-dir)"
45+
46+
export BINDGEN_EXTRA_CLANG_ARGS="$PHP_INCLUDES -isystem $CLANG_RESOURCE/include ${BINDGEN_EXTRA_CLANG_ARGS:-}"
47+
export CFLAGS="$PHP_INCLUDES -isystem $CLANG_RESOURCE/include ${CFLAGS:-}"
48+
49+
cargo build --release
50+
51+
EXT_SO="$EXT_DIR/target/release/libwp_mysql_parser.so"
52+
if [ ! -f "$EXT_SO" ]; then
53+
echo "Build completed but extension was not found at $EXT_SO" >&2
54+
exit 1
55+
fi
56+
57+
printf '%s\n' "$EXT_SO"
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
3+
const DEFAULT_QUERY_COUNT = 2000;
4+
const LONG_INSERT_ROWS = 2000;
5+
const TEST_TABLE = 'wp_lazy_native_test';
6+
7+
function smoke_ok( string $message ): void {
8+
echo "OK: {$message}\n";
9+
}
10+
11+
function smoke_fail( string $message ): void {
12+
fwrite( STDERR, "FAIL: {$message}\n" );
13+
exit( 1 );
14+
}
15+
16+
function create_driver(): WP_PDO_MySQL_On_SQLite {
17+
$db = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' );
18+
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
19+
$db->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true );
20+
return $db;
21+
}
22+
23+
function seed_database( WP_PDO_MySQL_On_SQLite $db ): void {
24+
$db->query(
25+
'CREATE TABLE `' . TEST_TABLE . "` (
26+
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
27+
`tenant_id` int NOT NULL,
28+
`label` varchar(191) NOT NULL,
29+
`score` int DEFAULT 0,
30+
`payload` text,
31+
PRIMARY KEY (`id`),
32+
KEY `tenant_score` (`tenant_id`, `score`),
33+
KEY `label_score` (`label`, `score`)
34+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
35+
);
36+
37+
for ( $i = 1; $i <= 64; $i++ ) {
38+
$db->query( make_insert_query( $i ) );
39+
}
40+
}
41+
42+
function sql_string( string $value ): string {
43+
return "'" . str_replace( "'", "''", $value ) . "'";
44+
}
45+
46+
function make_insert_query( int $i ): string {
47+
$tenant = $i % 17;
48+
$score = ( $i * 13 ) % 997;
49+
$label = sql_string( "label_{$i}_tenant_{$tenant}" );
50+
$payload = sql_string( '{"seed":' . $i . ',"tenant":' . $tenant . '}' );
51+
52+
return 'INSERT INTO `' . TEST_TABLE . "` (`tenant_id`, `label`, `score`, `payload`) VALUES ({$tenant}, {$label}, {$score}, {$payload})";
53+
}
54+
55+
function make_long_multi_insert_query( int $i ): string {
56+
$rows = array();
57+
for ( $row = 0; $row < LONG_INSERT_ROWS; $row++ ) {
58+
$tenant = ( $i + $row ) % 17;
59+
$score = ( $i * 31 + $row * 7 ) % 997;
60+
$label = sql_string( "bulk_{$i}_{$row}_tenant_{$tenant}" );
61+
$payload = sql_string( '{"bulk":' . $i . ',"row":' . $row . ',"tenant":' . $tenant . '}' );
62+
$rows[] = "({$tenant}, {$label}, {$score}, {$payload})";
63+
}
64+
65+
return 'INSERT INTO `' . TEST_TABLE . '` (`tenant_id`, `label`, `score`, `payload`) VALUES ' . implode( ",\n", $rows );
66+
}
67+
68+
function make_select_query( int $i ): string {
69+
$tenant = $i % 17;
70+
$min = ( $i * 7 ) % 400;
71+
$limit = ( $i % 5 ) + 1;
72+
73+
return 'SELECT `id`, `label`, `score`, CASE WHEN `score` >= ' . $min . " THEN 'high' ELSE 'low' END AS `bucket`
74+
FROM `" . TEST_TABLE . "`
75+
WHERE `tenant_id` = {$tenant}
76+
AND `score` BETWEEN {$min} AND 1000
77+
AND `label` LIKE 'label_%'
78+
ORDER BY `score` DESC, `id` ASC
79+
LIMIT {$limit}";
80+
}
81+
82+
function make_aggregate_query( int $i ): string {
83+
$tenant_a = $i % 17;
84+
$tenant_b = ( $i + 3 ) % 17;
85+
86+
return 'SELECT COUNT(*) AS `total`, COALESCE(MAX(`score`), 0) AS `max_score`, COALESCE(MIN(`score`), 0) AS `min_score`
87+
FROM `' . TEST_TABLE . "`
88+
WHERE (`tenant_id`, `score`) IN (({$tenant_a}, " . ( ( $i * 13 ) % 997 ) . "), ({$tenant_b}, " . ( ( $i * 17 ) % 997 ) . '))
89+
OR `label` IN (' . sql_string( "label_{$i}_tenant_{$tenant_a}" ) . ', ' . sql_string( "missing_{$i}" ) . ')';
90+
}
91+
92+
function make_update_query( int $i ): string {
93+
$tenant = $i % 17;
94+
$delta = ( $i % 5 ) + 1;
95+
$cutoff = ( $i * 11 ) % 997;
96+
97+
return 'UPDATE `' . TEST_TABLE . "`
98+
SET `score` = `score` + {$delta}
99+
WHERE `tenant_id` = {$tenant}
100+
AND `score` < {$cutoff}";
101+
}
102+
103+
function make_workload_query( int $i ): string {
104+
if ( 0 === $i % 250 ) {
105+
return make_long_multi_insert_query( $i );
106+
}
107+
108+
$factories = array(
109+
'make_insert_query',
110+
'make_select_query',
111+
'make_aggregate_query',
112+
'make_update_query',
113+
'make_select_query',
114+
'make_aggregate_query',
115+
);
116+
117+
$factory = $factories[ $i % count( $factories ) ];
118+
return $factory( $i + 64 );
119+
}
120+
121+
function parse_with_sqlite_driver( WP_PDO_MySQL_On_SQLite $db, string $sql ): int {
122+
$parser = $db->create_parser( $sql );
123+
$descendants = 0;
124+
125+
while ( $parser->next_query() ) {
126+
$ast = $parser->get_query_ast();
127+
if ( ! $ast instanceof WP_Parser_Node ) {
128+
smoke_fail( 'parser did not return a WP_Parser_Node' );
129+
}
130+
131+
$descendants += count( $ast->get_descendants() );
132+
}
133+
134+
return $descendants;
135+
}
136+
137+
function execute_with_sqlite_driver( WP_PDO_MySQL_On_SQLite $db, string $sql ): int {
138+
$result = $db->query( $sql );
139+
if ( ! $result instanceof PDOStatement ) {
140+
return 0;
141+
}
142+
143+
return count( $result->fetchAll( PDO::FETCH_ASSOC ) );
144+
}
145+
146+
function run_workload(): void {
147+
require_once dirname( __DIR__ ) . '/packages/mysql-on-sqlite/src/load.php';
148+
149+
$query_count = (int) ( getenv( 'TMP_TEST_NATIVE_QUERY_COUNT' ) ?: DEFAULT_QUERY_COUNT );
150+
$db = create_driver();
151+
seed_database( $db );
152+
153+
$start = microtime( true );
154+
$rows = 0;
155+
$descendants = 0;
156+
$long_inserts = 0;
157+
158+
for ( $i = 1; $i <= $query_count; $i++ ) {
159+
$sql = make_workload_query( $i );
160+
$long_inserts += 0 === $i % 250 ? 1 : 0;
161+
$descendants += parse_with_sqlite_driver( $db, $sql );
162+
$rows += execute_with_sqlite_driver( $db, $sql );
163+
}
164+
165+
if ( $descendants < $query_count * 10 ) {
166+
smoke_fail( "parsed too few descendants ({$descendants})" );
167+
}
168+
169+
smoke_ok(
170+
sprintf(
171+
'processed %d queries, including %d x %d-row multi-inserts, %d AST descendants, %d fetched rows in %.3fs',
172+
$query_count,
173+
$long_inserts,
174+
LONG_INSERT_ROWS,
175+
$descendants,
176+
$rows,
177+
microtime( true ) - $start
178+
)
179+
);
180+
}
181+
182+
run_workload();

tmp-test-native/run.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
7+
if ! grep -q 'WP_MySQL_Native_Ast' "$ROOT_DIR/packages/mysql-on-sqlite/ext/wp-mysql-parser/src/lib.rs"; then
8+
echo "This checkout does not include the lazy native AST facade." >&2
9+
echo "Switch to codex/native-lazy-ast-facade first." >&2
10+
exit 1
11+
fi
12+
13+
EXT_SO="$("$SCRIPT_DIR/build-extension.sh" | tail -n 1)"
14+
15+
echo "=== Current PHP configuration ==="
16+
php "$SCRIPT_DIR/run-sqlite-facade-smoke.php"
17+
18+
echo
19+
echo "=== Explicit native extension ==="
20+
php -d extension="$EXT_SO" "$SCRIPT_DIR/run-sqlite-facade-smoke.php"

0 commit comments

Comments
 (0)