Skip to content

Commit 37497be

Browse files
authored
Query Monitor support (#212)
This PR integrates [Query Monitor](https://github.com/johnbillion/query-monitor) so that it can be used together with the SQLite Database Integration plugin, and it extends the Query Monitor panel to include SQLite query information. This implementation uses some tricks and workarounds to achieve this functionality. We could improve the integration by proposing upstream changes to the Query Monitor plugin. #### The `wp-content/db.php` issue The main issue to overcome was that both plugins rely on the usage of the `wp-content/db.php` database drop-in file. To resolve this, I’ve enabled the SQLite plugin to override the `db.php` file created by Query Monitor while also ensuring it eagerly boots Query Monitor when needed. The behavior depends on which plugin is activated first: 1. If the **SQLite plugin** is activated first, its `db.php` file will never be replaced by Query Monitor. The SQLite plugin will detect when Query Monitor is active and boot it eagerly while also storing detailed query information for Query Monitor to consume. 2. If **Query Monitor** is activated first, the SQLite plugin will detect that on its activation, it will replace the existing `db.php` file, and it will retain the eager boot logic for Query Monitor. The user will be notified about the `db.php` file replacement. <p align="center"> <img width="600" alt="screenshot-2025-07-09-at-12 25 04" src="https://github.com/user-attachments/assets/4fa3a947-ceb4-43f8-a50e-d28096bf5b99" /> </p> #### Extended query information for Query Monitor The SQLite plugin now stores enhanced query information for Query Monitor when it’s active, effectively mirroring a tiny portion of the Query Monitor code. To reduce duplication, we can propose changes to the Query Monitor codebase that would allow this logic to be reused rather than reimplemented. #### Extending the Query Monitor panel The Query Monitor panel was extended to provide information about the executed SQLite queries for each MySQL query. This makes it easy to inspect and debug which SQLite queries correspond to each MySQL statement. Currently, Query Monitor doesn’t offer a way to customize the rendered query information. To overcome this limitation, I implemented this by capturing the generated HTML and modifying it before it is sent to the browser. This is another area where we could improve integration by proposing upstream changes to the Query Monitor plugin. ![sqlite-query-monitor](https://github.com/user-attachments/assets/113a0379-98f7-4bb9-b3ec-9a742c7ce0f0) #### Playground support [Playground loads the SQLite Database Integration differently](https://href.li/?https://github.com/WordPress/wordpress-playground/pull/1382), without creating a `db.php` file. As a result, third-party plugins can still inject their `db.php` drop-in. For Query Monitor, I’ve added a simple fix. Setting the `QM_DB_SYMLINK` constant to `false` prevents it from using the `db.php` drop-in, and the SQLite plugin will boot it as needed.
2 parents e49c578 + 5c2265e commit 37497be

File tree

7 files changed

+353
-45
lines changed

7 files changed

+353
-45
lines changed

activate.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ function sqlite_plugin_copy_db_file() {
8282

8383
$destination = WP_CONTENT_DIR . '/db.php';
8484

85+
/*
86+
* When an existing "db.php" drop-in is detected, let's check if it's a known
87+
* plugin that we can continue supporting even when we override the drop-in.
88+
*/
89+
$override_db_dropin = false;
90+
if ( file_exists( $destination ) ) {
91+
// Check for the Query Monitor plugin.
92+
// When "QM_DB" exists, it must have been loaded via the "db.php" file.
93+
if ( class_exists( 'QM_DB', false ) ) {
94+
$override_db_dropin = true;
95+
}
96+
97+
if ( $override_db_dropin ) {
98+
require_once ABSPATH . '/wp-admin/includes/file.php';
99+
global $wp_filesystem;
100+
if ( ! $wp_filesystem ) {
101+
WP_Filesystem();
102+
}
103+
$wp_filesystem->delete( $destination );
104+
}
105+
}
106+
85107
// Place database drop-in if not present yet, except in case there is
86108
// another database drop-in present already.
87109
if ( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) && ! file_exists( $destination ) ) {

admin-page.php

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ function sqlite_add_admin_menu() {
2626
* The admin page contents.
2727
*/
2828
function sqlite_integration_admin_screen() {
29+
$db_dropin_path = WP_CONTENT_DIR . '/db.php';
30+
31+
/*
32+
* When an existing "db.php" drop-in is detected, let's check if it's a known
33+
* plugin that we can continue supporting even when we override the drop-in.
34+
*/
35+
$override_db_dropin = false;
36+
if ( file_exists( $db_dropin_path ) && ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) {
37+
// Check for the Query Monitor plugin.
38+
// When "QM_DB" exists, it must have been loaded via the "db.php" file.
39+
if ( class_exists( 'QM_DB', false ) ) {
40+
$override_db_dropin = true;
41+
}
42+
}
43+
2944
?>
3045
<div class="wrap">
3146
<h1><?php esc_html_e( 'SQLite integration.', 'sqlite-database-integration' ); ?></h1>
@@ -46,68 +61,82 @@ function sqlite_integration_admin_screen() {
4661
);
4762
?>
4863
</p>
49-
<?php else : ?>
50-
<?php if ( ! extension_loaded( 'pdo_sqlite' ) ) : ?>
51-
<div class="notice notice-error">
52-
<p><?php esc_html_e( 'We detected that the PDO SQLite driver is missing from your server (the pdo_sqlite extension is not loaded). Please make sure that SQLite is enabled in your PHP installation before proceeding.', 'sqlite-database-integration' ); ?></p>
53-
</div>
54-
<?php elseif ( file_exists( WP_CONTENT_DIR . '/db.php' ) && ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) : ?>
55-
<?php if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) ) : ?>
56-
<div class="notice notice-warning">
57-
<p>
58-
<?php
59-
printf(
60-
/* translators: %s: db.php drop-in path */
61-
esc_html__( 'An older %s file was detected. Please click the button below to update the file.', 'sqlite-database-integration' ),
62-
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php</code>'
63-
);
64-
?>
65-
</p>
66-
</div>
67-
<a class="button button-primary" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=sqlite-integration&confirm-install&upgrade-from-pl' ), 'sqlite-install' ) ); ?>">
64+
<?php elseif ( ! extension_loaded( 'pdo_sqlite' ) ) : ?>
65+
<div class="notice notice-error">
66+
<p><?php esc_html_e( 'We detected that the PDO SQLite driver is missing from your server (the pdo_sqlite extension is not loaded). Please make sure that SQLite is enabled in your PHP installation before proceeding.', 'sqlite-database-integration' ); ?></p>
67+
</div>
68+
<?php elseif ( file_exists( $db_dropin_path ) && ! defined( 'SQLITE_DB_DROPIN_VERSION' ) && ! $override_db_dropin ) : ?>
69+
<?php if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) ) : ?>
70+
<div class="notice notice-warning">
71+
<p>
6872
<?php
6973
printf(
7074
/* translators: %s: db.php drop-in path */
71-
esc_html__( 'Update %s file', 'sqlite-database-integration' ),
75+
esc_html__( 'An older %s file was detected. Please click the button below to update the file.', 'sqlite-database-integration' ),
7276
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php</code>'
7377
);
7478
?>
75-
</a>
76-
<?php else : ?>
77-
<div class="notice notice-error">
78-
<p>
79-
<?php
80-
printf(
81-
/* translators: %s: db.php drop-in path */
82-
esc_html__( 'The SQLite plugin cannot be activated because a different %s drop-in already exists.', 'sqlite-database-integration' ),
83-
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php</code>'
84-
);
85-
?>
86-
</p>
87-
</div>
88-
<?php endif; ?>
89-
<?php elseif ( ! is_writable( WP_CONTENT_DIR ) ) : ?>
79+
</p>
80+
</div>
81+
<a class="button button-primary" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=sqlite-integration&confirm-install&upgrade-from-pl' ), 'sqlite-install' ) ); ?>">
82+
<?php
83+
printf(
84+
/* translators: %s: db.php drop-in path */
85+
esc_html__( 'Update %s file', 'sqlite-database-integration' ),
86+
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php</code>'
87+
);
88+
?>
89+
</a>
90+
<?php else : ?>
9091
<div class="notice notice-error">
9192
<p>
9293
<?php
9394
printf(
9495
/* translators: %s: db.php drop-in path */
95-
esc_html__( 'The SQLite plugin cannot be activated because the %s directory is not writable.', 'sqlite-database-integration' ),
96-
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '</code>'
96+
esc_html__( 'The SQLite plugin cannot be activated because a different %s drop-in already exists.', 'sqlite-database-integration' ),
97+
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php</code>'
9798
);
9899
?>
99100
</p>
100101
</div>
101-
<?php else : ?>
102-
<div class="notice notice-success">
103-
<p><?php esc_html_e( 'All checks completed successfully, your site can use an SQLite database. You can proceed with the installation.', 'sqlite-database-integration' ); ?></p>
104-
</div>
105-
<h2><?php esc_html_e( 'Important note', 'sqlite-database-integration' ); ?></h2>
106-
<p><?php esc_html_e( 'This plugin will switch to a separate database and install WordPress in it. You will need to reconfigure your site, and start with a fresh site. Disabling the plugin you will get back to your previous MySQL database, with all your previous data intact.', 'sqlite-database-integration' ); ?></p>
107-
<p><?php esc_html_e( 'By clicking the button below, you will be redirected to the WordPress installation screen to setup your new database', 'sqlite-database-integration' ); ?></p>
102+
<?php endif; ?>
103+
<?php elseif ( ! is_writable( WP_CONTENT_DIR ) ) : ?>
104+
<div class="notice notice-error">
105+
<p>
106+
<?php
107+
printf(
108+
/* translators: %s: db.php drop-in path */
109+
esc_html__( 'The SQLite plugin cannot be activated because the %s directory is not writable.', 'sqlite-database-integration' ),
110+
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '</code>'
111+
);
112+
?>
113+
</p>
114+
</div>
115+
<?php else : ?>
116+
<div class="notice notice-success">
117+
<p><?php esc_html_e( 'All checks completed successfully, your site can use an SQLite database. You can proceed with the installation.', 'sqlite-database-integration' ); ?></p>
118+
</div>
119+
<h2><?php esc_html_e( 'Important note', 'sqlite-database-integration' ); ?></h2>
120+
<p><?php esc_html_e( 'This plugin will switch to a separate database and install WordPress in it. You will need to reconfigure your site, and start with a fresh site. Disabling the plugin you will get back to your previous MySQL database, with all your previous data intact.', 'sqlite-database-integration' ); ?></p>
108121

109-
<a class="button button-primary" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=sqlite-integration&confirm-install' ), 'sqlite-install' ) ); ?>"><?php esc_html_e( 'Install SQLite database', 'sqlite-database-integration' ); ?></a>
122+
<?php if ( $override_db_dropin ) : ?>
123+
<p>
124+
<strong>NOTE:</strong>
125+
<?php
126+
printf(
127+
/* translators: %s: db.php drop-in path */
128+
esc_html__( 'We’ve detected an existing database drop-in file at %s, created by the Query Monitor plugin.', 'sqlite-database-integration' ),
129+
'<code>' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php</code>'
130+
);
131+
?>
132+
<?php esc_html_e( 'To enable SQLite support, this file will need to be replaced.', 'sqlite-database-integration' ); ?>
133+
<?php esc_html_e( 'The Query Monitor plugin will continue to function correctly after the change. You can safely proceed with the installation.', 'sqlite-database-integration' ); ?>
134+
</p>
110135
<?php endif; ?>
136+
137+
<p><?php esc_html_e( 'By clicking the button below, you will be redirected to the WordPress installation screen to setup your new database', 'sqlite-database-integration' ); ?></p>
138+
139+
<a class="button button-primary" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=sqlite-integration&confirm-install' ), 'sqlite-install' ) ); ?>"><?php esc_html_e( 'Install SQLite database', 'sqlite-database-integration' ); ?></a>
111140
<?php endif; ?>
112141
</div>
113142
<?php
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/**
4+
* Boot Query Monitor from the SQLite Database Integration plugin.
5+
*
6+
* When the Query Monitor plugin exists in its standard location, let's check
7+
* if it is active, so we can boot it eagerly. This is a workaround to avoid
8+
* SQLite and Query Monitor competing for the "wp-content/db.php" file.
9+
*
10+
* This file is a modified version of the original Query Monitor "db.php" file.
11+
*
12+
* See: https://github.com/johnbillion/query-monitor/blob/develop/wp-content/db.php
13+
*/
14+
15+
/*
16+
* In Playground, the SQLite plugin is preloaded without using the "db.php" file.
17+
* To prevent Query Monitor from injecting its own "db.php" file, we need to set
18+
* the "QM_DB_SYMLINK" constant to "false".
19+
*/
20+
if ( ! defined( 'QM_DB_SYMLINK' ) ) {
21+
define( 'QM_DB_SYMLINK', false );
22+
}
23+
24+
// 1. Check if we should load Query Monitor (as per the original "db.php" file).
25+
if ( ! defined( 'ABSPATH' ) ) {
26+
exit;
27+
}
28+
29+
if ( ! defined( 'DB_USER' ) ) {
30+
return;
31+
}
32+
33+
if ( defined( 'QM_DISABLED' ) && QM_DISABLED ) {
34+
return;
35+
}
36+
37+
if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) {
38+
return;
39+
}
40+
41+
if ( 'cli' === php_sapi_name() && ! defined( 'QM_TESTS' ) ) {
42+
return;
43+
}
44+
45+
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
46+
return;
47+
}
48+
49+
if ( is_admin() ) {
50+
if ( isset( $_GET['action'] ) && 'upgrade-plugin' === $_GET['action'] ) {
51+
return;
52+
}
53+
54+
if ( isset( $_POST['action'] ) && 'update-plugin' === $_POST['action'] ) {
55+
return;
56+
}
57+
}
58+
59+
// 2. Check if Query Monitor is active.
60+
if ( null === $wpdb->options ) {
61+
global $table_prefix;
62+
$wpdb->set_prefix( $table_prefix ?? '' );
63+
}
64+
65+
$query_monitor_active = false;
66+
try {
67+
$value = $wpdb->get_row(
68+
$wpdb->prepare(
69+
"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
70+
'active_plugins'
71+
)
72+
);
73+
$query_monitor_active = in_array(
74+
'query-monitor/query-monitor.php',
75+
unserialize( $value->option_value ),
76+
true
77+
);
78+
} catch ( Throwable $e ) {
79+
return;
80+
}
81+
82+
if ( ! $query_monitor_active ) {
83+
return;
84+
}
85+
86+
// 3. Determine the plugins directory.
87+
if ( defined( 'WP_PLUGIN_DIR' ) ) {
88+
$plugins_dir = WP_PLUGIN_DIR;
89+
} else {
90+
$plugins_dir = WP_CONTENT_DIR . '/plugins';
91+
}
92+
93+
// 4. Load Query Monitor (as per the original "db.php" file).
94+
$qm_dir = "{$plugins_dir}/query-monitor";
95+
$qm_php = "{$qm_dir}/classes/PHP.php";
96+
97+
if ( ! is_readable( $qm_php ) ) {
98+
return;
99+
}
100+
require_once $qm_php;
101+
102+
if ( ! QM_PHP::version_met() ) {
103+
return;
104+
}
105+
106+
if ( ! file_exists( "{$qm_dir}/vendor/autoload.php" ) ) {
107+
add_action( 'all_admin_notices', 'QM_PHP::vendor_nope' );
108+
return;
109+
}
110+
111+
require_once "{$qm_dir}/vendor/autoload.php";
112+
113+
if ( ! class_exists( 'QM_Backtrace' ) ) {
114+
return;
115+
}
116+
117+
if ( ! defined( 'SAVEQUERIES' ) ) {
118+
define( 'SAVEQUERIES', true );
119+
}
120+
121+
// 5. Mark the Query Monitor integration as loaded.
122+
define( 'SQLITE_QUERY_MONITOR_LOADED', true );
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
2+
3+
if ( ! class_exists( 'QM_Output_Html_DB_Queries' ) || ! class_exists( 'QM_Collectors' ) ) {
4+
return;
5+
}
6+
7+
/**
8+
* Override Query Monitor's "QM_Output_Html_DB_Queries" to inject SQLite info.
9+
*/
10+
class SQLite_QM_Output_Html_DB_Queries extends QM_Output_Html_DB_Queries {
11+
/**
12+
* Override the parent method to inject SQLite info.
13+
*
14+
* Currently, Query Monitor doesn't provide a way to customize the rendered
15+
* query HTML. To overcome this limitation, we capture the HTML generated by
16+
* the parent method and modify it to inject the SQLite query data.
17+
*
18+
* @param array<string, mixed> $row The row data.
19+
* @param array<int, string> $cols The column names.
20+
* @return void
21+
*/
22+
protected function output_query_row( array $row, array $cols ) {
23+
// Capture the query row HTML.
24+
ob_start();
25+
parent::output_query_row( $row, $cols );
26+
$data = ob_get_length() > 0 ? ob_get_clean() : '';
27+
28+
// Get the corresponding SQLite queries.
29+
global $wpdb;
30+
static $query_index = 0;
31+
$sqlite_queries = $wpdb->queries[ $query_index ]['sqlite_queries'] ?? array();
32+
$sqlite_query_count = count( $sqlite_queries );
33+
$query_index += 1;
34+
35+
// Build the SQLite info HTML.
36+
$sqlite_info = sprintf(
37+
'<div class="qm-info" style="margin: 15px 0 8px;">Executed %d SQLite %s:</div>',
38+
$sqlite_query_count,
39+
1 === $sqlite_query_count ? 'Query' : 'Queries'
40+
);
41+
$sqlite_info .= '<ol>';
42+
foreach ( $sqlite_queries as $query ) {
43+
$sqlite_info .= '<li class="qm-sqlite-query" style="list-style: decimal !important; margin-left: 20px !important;">';
44+
$sqlite_info .= '<code>' . str_replace( '<br>', '', self::format_sql( $query['sql'] ) ) . '</code>';
45+
$sqlite_info .= '</li>';
46+
}
47+
$sqlite_info .= '</ol>';
48+
49+
// Inject toggle button and SQLite info into the query row HTML.
50+
$toggle_button = '<button class="qm-toggle sqlite-toggle" data-on="+" data-off="-" aria-expanded="false" aria-label="Toggle SQLite queries" title="Toggle SQLite queries"><span aria-hidden="true">+</span></button>';
51+
$toggle_content = sprintf( '<div class="qm-toggled" style="display: none;">%s</div>', $sqlite_info );
52+
53+
$data = str_replace( 'qm-row-sql', 'qm-row-sql qm-has-toggle', $data );
54+
$data = preg_replace(
55+
'/(<td class="qm-row-sql.*?">)(.*?)(<\/td>)/s',
56+
implode(
57+
array(
58+
'$1',
59+
str_replace( '$', '\\$', $toggle_button ),
60+
'$2',
61+
str_replace( '$', '\\$', $toggle_content ),
62+
'$3',
63+
)
64+
),
65+
$data
66+
);
67+
echo $data;
68+
}
69+
}
70+
71+
// Remove the default Query Monitor class and replace it with the custom one.
72+
remove_filter( 'qm/outputter/html', 'register_qm_output_html_db_queries', 20 );
73+
74+
/**
75+
* Register the custom HTML output class.
76+
*
77+
* @param array<string, QM_Output> $output
78+
* @param QM_Collectors $collectors
79+
* @return array<string, QM_Output>
80+
*/
81+
function register_sqlite_qm_output_html_db_queries( array $output, $collectors ) {
82+
$collector = QM_Collectors::get( 'db_queries' );
83+
if ( $collector ) {
84+
$output['db_queries'] = new SQLite_QM_Output_Html_DB_Queries( $collector );
85+
}
86+
return $output;
87+
}
88+
89+
add_filter( 'qm/outputter/html', 'register_sqlite_qm_output_html_db_queries', 20, 2 );

0 commit comments

Comments
 (0)