diff --git a/features/db-check.feature b/features/db-check.feature
index aa5ce47b..d1f7ba80 100644
--- a/features/db-check.feature
+++ b/features/db-check.feature
@@ -1,5 +1,6 @@
Feature: Check the database
+ @require-mysql-or-mariadb
Scenario: Run db check to check the database
Given a WP install
@@ -13,6 +14,7 @@ Feature: Check the database
Success: Database checked.
"""
+ @require-mysql-or-mariadb
Scenario: Run db check with MySQL defaults to check the database
Given a WP install
@@ -26,6 +28,7 @@ Feature: Check the database
Success: Database checked.
"""
+ @require-mysql-or-mariadb
Scenario: Run db check with --no-defaults to check the database
Given a WP install
@@ -39,6 +42,7 @@ Feature: Check the database
Success: Database checked.
"""
+ @require-mysql-or-mariadb
Scenario: Run db check with passed-in options
Given a WP install
@@ -124,6 +128,7 @@ Feature: Check the database
"""
And STDOUT should be empty
+ @require-mysql-or-mariadb
Scenario: MySQL defaults are available as appropriate with --defaults flag
Given a WP install
@@ -136,3 +141,12 @@ Feature: Check the database
When I try `wp db check --no-defaults --debug`
Then STDERR should match #Debug \(db\): Running shell command: /usr/bin/env (mysqlcheck|mariadb-check) --no-defaults %s#
+ @require-sqlite
+ Scenario: SQLite commands that show warnings
+ Given a WP install
+
+ When I try `wp db check`
+ Then STDERR should contain:
+ """
+ Warning: Database check is not supported for SQLite databases
+ """
diff --git a/features/db-cli.feature b/features/db-cli.feature
new file mode 100644
index 00000000..4ef8b885
--- /dev/null
+++ b/features/db-cli.feature
@@ -0,0 +1,11 @@
+Feature: Open a MySQL console
+
+ @require-sqlite
+ Scenario: SQLite commands that show warnings for cli
+ Given a WP install
+
+ When I try `wp db cli`
+ Then STDERR should contain:
+ """
+ Warning: Interactive console (cli) is not supported for SQLite databases
+ """
diff --git a/features/db-columns.feature b/features/db-columns.feature
index b2e1ee76..8372451f 100644
--- a/features/db-columns.feature
+++ b/features/db-columns.feature
@@ -41,6 +41,7 @@ Feature: Display information about a given table.
Couldn't find any tables matching: wp_foobar
"""
+ @require-mysql-or-mariadb
Scenario: Display information about a non default WordPress table
Given a WP install
And I run `wp db query "CREATE TABLE not_wp ( date DATE NOT NULL, awesome_stuff TEXT, PRIMARY KEY (date) );;"`
@@ -50,3 +51,14 @@ Feature: Display information about a given table.
| Field | Type | Null | Key | Default | Extra |
| date | date | NO | PRI | | |
| awesome_stuff | text | YES | | | |
+
+ @require-sqlite
+ Scenario: Display information about a non default WordPress table
+ Given a WP install
+ And I run `wp db query "CREATE TABLE not_wp ( date DATE NOT NULL, awesome_stuff TEXT, PRIMARY KEY (date) );;"`
+
+ When I try `wp db columns not_wp`
+ Then STDOUT should be a table containing rows:
+ | Field | Type | Null | Key | Default |
+ | date | TEXT | NO | PRI | '' |
+ | awesome_stuff | TEXT | YES | | |
diff --git a/features/db-create.feature b/features/db-create.feature
new file mode 100644
index 00000000..8b90873a
--- /dev/null
+++ b/features/db-create.feature
@@ -0,0 +1,24 @@
+Feature: Create a new database
+
+ @require-mysql-or-mariadb
+ Scenario: Create a new database
+ Given an empty directory
+ And WP files
+ And wp-config.php
+
+ When I run `wp db create`
+ Then STDOUT should contain:
+ """
+ Success: Database created.
+ """
+
+ @require-sqlite
+ Scenario: SQLite DB create operation should fail if already existing
+ Given a WP install
+
+ When I try `wp db create`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ Database already exists
+ """
diff --git a/features/db-export.feature b/features/db-export.feature
index 370ae1df..45dfd9ad 100644
--- a/features/db-export.feature
+++ b/features/db-export.feature
@@ -23,16 +23,10 @@ Feature: Export a WordPress database
Scenario: Exclude tables when exporting the database
Given a WP install
- When I run `wp db export wp_cli_test.sql --exclude_tables=wp_users --porcelain`
+ When I try `wp db export wp_cli_test.sql --exclude_tables=wp_users --porcelain`
Then the wp_cli_test.sql file should exist
- And the wp_cli_test.sql file should not contain:
- """
- wp_users
- """
- And the wp_cli_test.sql file should contain:
- """
- wp_options
- """
+ And the contents of the wp_cli_test.sql file should not match /CREATE TABLE ["`]?wp_users["`]?/
+ And the contents of the wp_cli_test.sql file should match /CREATE TABLE ["`]?wp_options["`]?/
Scenario: Export database to STDOUT
Given a WP install
@@ -61,6 +55,7 @@ Feature: Export a WordPress database
-- Dump completed on
"""
+ @require-mysql-or-mariadb
Scenario: Export database with passed-in options
Given a WP install
@@ -78,6 +73,25 @@ Feature: Export a WordPress database
"""
And STDOUT should be empty
+ @require-sqlite
+ Scenario: Export database with passed-in options
+ Given a WP install
+
+ When I run `wp db export - --skip-comments`
+ Then STDOUT should not contain:
+ """
+ -- Table structure
+ """
+
+ # dbpass has no effect on SQLite
+ When I try `wp db export - --dbpass=no_such_pass`
+ Then the return code should be 0
+ And STDERR should not contain:
+ """
+ Access denied
+ """
+
+ @require-mysql-or-mariadb
Scenario: MySQL defaults are available as appropriate with --defaults flag
Given a WP install
diff --git a/features/db-import.feature b/features/db-import.feature
index 5761fb24..75ce74f3 100644
--- a/features/db-import.feature
+++ b/features/db-import.feature
@@ -127,6 +127,8 @@ Feature: Import a WordPress database
"""
wp db import
"""
+
+ @require-mysql-or-mariadb
Scenario: MySQL defaults are available as appropriate with --defaults flag
Given a WP install
@@ -142,7 +144,7 @@ Feature: Import a WordPress database
When I try `wp db import --no-defaults --debug`
Then STDERR should match #Debug \(db\): Running shell command: /usr/bin/env (mysql|mariadb) --no-defaults --no-auto-rehash#
- @require-wp-4.2
+ @require-wp-4.2 @require-mysql-or-mariadb
Scenario: Import db that has emoji in post
Given a WP install
@@ -178,3 +180,32 @@ Feature: Import a WordPress database
"""
🍣
"""
+
+ @require-wp-4.2 @require-sqlite
+ Scenario: Import db that has emoji in post
+ Given a WP install
+
+ When I run `wp post create --post_title="🍣"`
+ And I run `wp post list`
+ Then the return code should be 0
+ And STDOUT should contain:
+ """
+ 🍣
+ """
+
+ When I try `wp db export wp_cli_test.sql --debug`
+ Then the return code should be 0
+ And the wp_cli_test.sql file should exist
+
+ When I run `wp db import --dbuser=wp_cli_test --dbpass=password1`
+ Then STDOUT should be:
+ """
+ Success: Imported from 'wp_cli_test.sql'.
+ """
+
+ When I run `wp post list`
+ Then the return code should be 0
+ And STDOUT should contain:
+ """
+ 🍣
+ """
diff --git a/features/db-optimize.feature b/features/db-optimize.feature
new file mode 100644
index 00000000..4fec46d3
--- /dev/null
+++ b/features/db-optimize.feature
@@ -0,0 +1,21 @@
+Feature: Optimize the database
+
+ @require-mysql-or-mariadb
+ Scenario: Run db optimize to optimize the database
+ Given a WP install
+
+ When I run `wp db optimize`
+ Then STDOUT should contain:
+ """
+ Success: Database optimized.
+ """
+
+ @require-sqlite
+ Scenario: SQLite commands that show warnings for optimize
+ Given a WP install
+
+ When I try `wp db optimize`
+ Then STDERR should contain:
+ """
+ Warning: Database optimization is not supported for SQLite databases
+ """
diff --git a/features/db-query.feature b/features/db-query.feature
index 5831a536..2f518995 100644
--- a/features/db-query.feature
+++ b/features/db-query.feature
@@ -72,6 +72,7 @@ Feature: Query the database with WordPress' MySQL config
"""
And STDOUT should be empty
+ @require-mysql-or-mariadb
Scenario: MySQL defaults are available as appropriate with --defaults flag
Given a WP install
diff --git a/features/db-repair.feature b/features/db-repair.feature
new file mode 100644
index 00000000..46104b21
--- /dev/null
+++ b/features/db-repair.feature
@@ -0,0 +1,21 @@
+Feature: Repair the database
+
+ @require-mysql-or-mariadb
+ Scenario: Run db repair to repair the database
+ Given a WP install
+
+ When I run `wp db repair`
+ Then STDOUT should contain:
+ """
+ Success: Database repaired.
+ """
+
+ @require-sqlite
+ Scenario: SQLite commands that show warnings for repair
+ Given a WP install
+
+ When I try `wp db repair`
+ Then STDERR should contain:
+ """
+ Warning: Database repair is not supported for SQLite databases
+ """
diff --git a/features/db-size.feature b/features/db-size.feature
index 0958bbf1..2728fc17 100644
--- a/features/db-size.feature
+++ b/features/db-size.feature
@@ -2,6 +2,7 @@
Feature: Display database size
+ @require-mysql-or-mariadb
Scenario: Display only database size for a WordPress install
Given a WP install
@@ -16,6 +17,21 @@ Feature: Display database size
B
"""
+ @require-sqlite
+ Scenario: Display only database size for a WordPress install
+ Given a WP install
+
+ When I run `wp db size`
+ Then STDOUT should contain:
+ """
+ .ht.sqlite
+ """
+
+ And STDOUT should contain:
+ """
+ B
+ """
+
Scenario: Display only table sizes for a WordPress install
Given a WP install
@@ -27,6 +43,7 @@ Feature: Display database size
wp_cli_test
"""
+ @require-mysql-or-mariadb
Scenario: Display only database size in a human readable format for a WordPress install
Given a WP install
@@ -49,6 +66,21 @@ Feature: Display database size
"""
And STDOUT should be empty
+ @require-sqlite
+ Scenario: Display only database size in a human readable format for a WordPress install
+ Given a WP install
+
+ When I run `wp db size --human-readable`
+ Then STDOUT should contain:
+ """
+ .ht.sqlite
+ """
+
+ And STDOUT should contain:
+ """
+ KB
+ """
+
Scenario: Display only table sizes in a human readable format for a WordPress install
Given a WP install
@@ -155,6 +187,7 @@ Feature: Display database size
MB
"""
+ @require-mysql-or-mariadb
Scenario: Display database size in bytes with specific format for a WordPress install
Given a WP install
@@ -175,6 +208,27 @@ Feature: Display database size
But STDOUT should not be a number
+ @require-sqlite
+ Scenario: Display database size in bytes with specific format for a WordPress install
+ Given a WP install
+
+ When I run `wp db size --size_format=b --format=csv`
+ Then STDOUT should contain:
+ """
+ Name,Size
+ .ht.sqlite,"
+ """
+
+ But STDOUT should not be a number
+
+ When I run `wp db size --size_format=b --format=json`
+ Then STDOUT should contain:
+ """
+ [{"Name":".ht.sqlite","Size":"
+ """
+
+ But STDOUT should not be a number
+
Scenario: Display all table sizes for a WordPress install
Given a WP install
diff --git a/features/db-tables.feature b/features/db-tables.feature
index af8cc6ac..8c93bc0d 100644
--- a/features/db-tables.feature
+++ b/features/db-tables.feature
@@ -1,5 +1,6 @@
Feature: List database tables
+ @require-mysql-or-mariadb
Scenario: List database tables on a single WordPress install
Given a WP install
@@ -35,7 +36,42 @@ Feature: List database tables
wp_postmeta,wp_posts
"""
- @require-wp-3.9
+ @require-sqlite
+ Scenario: List database tables on a single WordPress install
+ Given a WP install
+
+ When I run `wp db tables`
+ Then STDOUT should contain:
+ """
+ _mysql_data_types_cache
+ wp_users
+ sqlite_sequence
+ wp_usermeta
+ wp_termmeta
+ wp_terms
+ wp_term_taxonomy
+ wp_term_relationships
+ wp_commentmeta
+ wp_comments
+ wp_links
+ wp_options
+ wp_postmeta
+ wp_posts
+ """
+
+ When I run `wp db tables --format=csv`
+ Then STDOUT should contain:
+ """
+ ,wp_commentmeta,wp_comments,
+ """
+
+ When I run `wp db tables 'wp_post*' --format=csv`
+ Then STDOUT should be:
+ """
+ wp_postmeta,wp_posts
+ """
+
+ @require-wp-3.9 @require-mysql-or-mariadb
Scenario: List database tables on a multisite WordPress install
Given a WP multisite install
@@ -119,6 +155,86 @@ Feature: List database tables
wp_posts
"""
+ @require-sqlite
+ Scenario: List database tables on a multisite WordPress install
+ Given a WP multisite install
+
+ When I run `wp db tables`
+ Then STDOUT should contain:
+ """
+ _mysql_data_types_cache
+ wp_users
+ sqlite_sequence
+ wp_usermeta
+ wp_termmeta
+ wp_terms
+ wp_term_taxonomy
+ wp_term_relationships
+ wp_commentmeta
+ wp_comments
+ wp_links
+ wp_options
+ wp_postmeta
+ wp_posts
+ wp_blogs
+ wp_blogmeta
+ wp_registration_log
+ wp_site
+ wp_sitemeta
+ wp_signups
+ """
+
+ When I run `wp site create --slug=foo`
+ And I run `wp db tables --url=example.com/foo`
+ Then STDOUT should contain:
+ """
+ wp_users
+ """
+ And STDOUT should contain:
+ """
+ wp_usermeta
+ """
+ And STDOUT should contain:
+ """
+ wp_2_posts
+ """
+
+ When I run `wp db tables --url=example.com/foo --scope=global`
+ Then STDOUT should not contain:
+ """
+ wp_2_posts
+ """
+
+ When I run `wp db tables --all-tables-with-prefix`
+ Then STDOUT should contain:
+ """
+ wp_2_posts
+ """
+ And STDOUT should contain:
+ """
+ wp_posts
+ """
+
+ When I run `wp db tables --url=example.com/foo --all-tables-with-prefix`
+ Then STDOUT should contain:
+ """
+ wp_2_posts
+ """
+ And STDOUT should not contain:
+ """
+ wp_posts
+ """
+
+ When I run `wp db tables --url=example.com/foo --network`
+ Then STDOUT should contain:
+ """
+ wp_2_posts
+ """
+ And STDOUT should contain:
+ """
+ wp_posts
+ """
+
Scenario: Listing a site's tables should only list that site's tables
Given a WP multisite install
diff --git a/features/db.feature b/features/db.feature
index dbfdb4b0..2d47ba1e 100644
--- a/features/db.feature
+++ b/features/db.feature
@@ -1,5 +1,6 @@
Feature: Perform database operations
+ @require-mysql-or-mariadb
Scenario: DB CRUD
Given an empty directory
And WP files
@@ -52,6 +53,7 @@ Feature: Perform database operations
Are you sure you want to reset the 'wp_cli_test' database? [y/n] Success: Database reset.
"""
+ @require-mysql-or-mariadb
Scenario: DB CRUD with passed-in dbuser/dbpass
Given an empty directory
And WP files
@@ -107,6 +109,7 @@ Feature: Perform database operations
"""
And STDOUT should be empty
+ @require-mysql-or-mariadb
Scenario: Clean up a WordPress install without dropping its database entirely but tables with prefix.
Given a WP install
@@ -138,6 +141,7 @@ Feature: Perform database operations
"""
And the return code should be 0
+ @require-mysql-or-mariadb
Scenario: DB Operations
Given a WP install
@@ -147,6 +151,7 @@ Feature: Perform database operations
When I run `wp db repair`
Then STDOUT should not be empty
+ @require-mysql-or-mariadb
Scenario: DB Operations with passed-in options
Given a WP install
@@ -185,6 +190,7 @@ Feature: Perform database operations
"""
And STDOUT should not be empty
+ @require-mysql-or-mariadb
Scenario: DB Query
Given a WP install
@@ -214,6 +220,7 @@ Feature: Perform database operations
home
"""
+ @require-mysql-or-mariadb
Scenario: DB export/import
Given a WP install
@@ -262,6 +269,7 @@ Feature: Perform database operations
1
"""
+ @require-mysql-or-mariadb
Scenario: DB export no charset
Given an empty directory
And WP files
@@ -327,6 +335,7 @@ Feature: Perform database operations
latin1_spanish_ci
"""
+ @require-mysql-or-mariadb
Scenario: Row modifying queries should return the number of affected rows
Given a WP install
When I run `wp db query "UPDATE wp_users SET user_status = 1 WHERE ID = 1"`
@@ -334,7 +343,7 @@ Feature: Perform database operations
"""
Query succeeded. Rows affected: 1
"""
-
+
When I run `wp db query "SELECT * FROM wp_users WHERE ID = 1"`
Then STDOUT should not contain:
"""
@@ -346,3 +355,74 @@ Feature: Perform database operations
"""
Query succeeded. Rows affected: 1
"""
+
+ @require-sqlite
+ Scenario: SQLite DB CRUD operations
+ Given a WP install
+ And a session_yes file:
+ """
+ y
+ """
+
+ When I try `wp db create`
+ Then the return code should be 1
+ And STDERR should contain:
+ """
+ Database already exists
+ """
+
+ When I run `wp db drop < session_yes`
+ Then STDOUT should contain:
+ """
+ Success: Database dropped.
+ """
+
+ When I run `wp db reset < session_yes`
+ Then STDOUT should contain:
+ """
+ Success: Database reset
+ """
+
+ @require-sqlite
+ Scenario: SQLite DB query
+ Given a WP install
+
+ When I run `wp db query 'SELECT COUNT(*) as total FROM wp_posts'`
+ Then STDOUT should contain:
+ """
+ total
+ """
+
+ @require-sqlite
+ Scenario: SQLite DB export/import
+ Given a WP install
+
+ When I run `wp post list --format=count`
+ Then STDOUT should contain:
+ """
+ 1
+ """
+
+ When I run `wp db export /tmp/wp-cli-sqlite-behat.sql`
+ Then STDOUT should contain:
+ """
+ Success: Exported
+ """
+
+ When I run `wp db reset < session_yes`
+ Then STDOUT should contain:
+ """
+ Success: Database reset
+ """
+
+ When I run `wp db import /tmp/wp-cli-sqlite-behat.sql`
+ Then STDOUT should contain:
+ """
+ Success: Imported
+ """
+
+ When I run `wp post list --format=count`
+ Then STDOUT should contain:
+ """
+ 1
+ """
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index c39cec40..a398187a 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -55,6 +55,13 @@
*/src/DB_Command\.php$
+
+ */src/DB_Command_SQLite\.php$
+
+
+
+ */src/DB_Command_SQLite\.php$
+
/tests/phpstan/scan-files
diff --git a/src/DB_Command.php b/src/DB_Command.php
index 06781cee..08b2a15a 100644
--- a/src/DB_Command.php
+++ b/src/DB_Command.php
@@ -3,6 +3,8 @@
use WP_CLI\Formatter;
use WP_CLI\Utils;
+require_once __DIR__ . '/DB_Command_SQLite.php';
+
/**
* Performs basic database operations using credentials stored in wp-config.php.
*
@@ -27,6 +29,8 @@
*/
class DB_Command extends WP_CLI_Command {
+ use DB_Command_SQLite;
+
/**
* Legacy UTF-8 encoding for MySQL.
*
@@ -82,6 +86,12 @@ class DB_Command extends WP_CLI_Command {
* Success: Database created.
*/
public function create( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ $this->sqlite_create();
+ return;
+ }
$this->run_query( self::get_create_query(), $assoc_args );
@@ -115,6 +125,15 @@ public function create( $_, $assoc_args ) {
* Success: Database dropped.
*/
public function drop( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ $db_path = $this->get_sqlite_db_path();
+ WP_CLI::confirm( "Are you sure you want to drop the SQLite database at '{$db_path}'?", $assoc_args );
+ $this->sqlite_drop();
+ return;
+ }
+
WP_CLI::confirm( "Are you sure you want to drop the '" . DB_NAME . "' database?", $assoc_args );
$this->run_query( sprintf( 'DROP DATABASE `%s`', DB_NAME ), $assoc_args );
@@ -149,6 +168,15 @@ public function drop( $_, $assoc_args ) {
* Success: Database reset.
*/
public function reset( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ $db_path = $this->get_sqlite_db_path();
+ WP_CLI::confirm( "Are you sure you want to reset the SQLite database at '{$db_path}'?", $assoc_args );
+ $this->sqlite_reset();
+ return;
+ }
+
WP_CLI::confirm( "Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args );
$this->run_query( sprintf( 'DROP DATABASE IF EXISTS `%s`', DB_NAME ), $assoc_args );
@@ -249,6 +277,12 @@ public function clean( $_, $assoc_args ) {
* Success: Database checked.
*/
public function check( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ WP_CLI::warning( 'Database check is not supported for SQLite databases.' );
+ return;
+ }
$command = sprintf(
'/usr/bin/env %s%s %s',
@@ -298,6 +332,13 @@ public function check( $_, $assoc_args ) {
* Success: Database optimized.
*/
public function optimize( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ WP_CLI::warning( 'Database optimization is not supported for SQLite databases. SQLite automatically optimizes on VACUUM.' );
+ return;
+ }
+
$command = sprintf(
'/usr/bin/env %s%s %s',
Utils\get_sql_check_command(),
@@ -346,6 +387,13 @@ public function optimize( $_, $assoc_args ) {
* Success: Database repaired.
*/
public function repair( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ WP_CLI::warning( 'Database repair is not supported for SQLite databases.' );
+ return;
+ }
+
$command = sprintf(
'/usr/bin/env %s%s %s',
Utils\get_sql_check_command(),
@@ -396,6 +444,12 @@ public function repair( $_, $assoc_args ) {
* @alias connect
*/
public function cli( $_, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ WP_CLI::warning( 'Interactive console (cli) is not supported for SQLite databases. Use `wp db query` instead.' );
+ return;
+ }
$command = sprintf(
'/usr/bin/env %s%s --no-auto-rehash',
@@ -499,6 +553,24 @@ public function cli( $_, $assoc_args ) {
* +---------+-----------------------+
*/
public function query( $args, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
+ if ( $this->is_sqlite() ) {
+ // Get the query from args or STDIN.
+ $query = '';
+ if ( ! empty( $args ) ) {
+ $query = $args[0];
+ } else {
+ $query = stream_get_contents( STDIN );
+ }
+
+ if ( empty( $query ) ) {
+ WP_CLI::error( 'No query specified.' );
+ }
+
+ $this->sqlite_query( $query );
+ return;
+ }
$command = sprintf(
'/usr/bin/env %s%s --no-auto-rehash',
@@ -629,6 +701,8 @@ public function query( $args, $assoc_args ) {
* @alias dump
*/
public function export( $args, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
if ( ! empty( $args[0] ) ) {
$result_file = $args[0];
} else {
@@ -637,6 +711,12 @@ public function export( $args, $assoc_args ) {
$result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
}
+
+ if ( $this->is_sqlite() ) {
+ $this->sqlite_export( $result_file, $assoc_args );
+ return;
+ }
+
$stdout = ( '-' === $result_file );
$porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' );
@@ -798,12 +878,19 @@ private function get_posts_table_charset( $assoc_args ) {
* Success: Imported from 'wordpress_dbase.sql'.
*/
public function import( $args, $assoc_args ) {
+ $this->maybe_load_sqlite_dropin();
+
if ( ! empty( $args[0] ) ) {
$result_file = $args[0];
} else {
$result_file = sprintf( '%s.sql', DB_NAME );
}
+ if ( $this->is_sqlite() ) {
+ $this->sqlite_import( $result_file );
+ return;
+ }
+
// Process options to MySQL.
$mysql_args = array_merge(
[ 'database' => DB_NAME ],
@@ -1080,19 +1167,30 @@ public function size( $args, $assoc_args ) {
$default_unit = ( empty( $size_format ) && ! $human_readable ) ? ' B' : '';
+ $is_sqlite = $this->is_sqlite();
+
if ( $tables || $all_tables || $all_tables_with_prefix ) {
// Add all of the table sizes.
foreach ( Utils\wp_get_table_names( $args, $assoc_args ) as $table_name ) {
// Get the table size.
- $table_bytes = $wpdb->get_var(
- $wpdb->prepare(
- 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s and Table_Name = %s GROUP BY Table_Name LIMIT 1',
- DB_NAME,
- $table_name
- )
- );
+ if ( $is_sqlite ) {
+ $table_bytes = $wpdb->get_var(
+ $wpdb->prepare(
+ 'SELECT SUM(pgsize) as size_in_bytes FROM dbstat where name = %s LIMIT 1',
+ $table_name
+ )
+ );
+ } else {
+ $table_bytes = $wpdb->get_var(
+ $wpdb->prepare(
+ 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s and Table_Name = %s GROUP BY Table_Name LIMIT 1',
+ DB_NAME,
+ $table_name
+ )
+ );
+ }
// Add the table size to the list.
$rows[] = [
@@ -1104,16 +1202,23 @@ public function size( $args, $assoc_args ) {
} else {
// Get the database size.
- $db_bytes = $wpdb->get_var(
- $wpdb->prepare(
- 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s GROUP BY table_schema;',
- DB_NAME
- )
- );
+ if ( $is_sqlite ) {
+ $db_bytes = $this->sqlite_size();
+ $db_path = $this->get_sqlite_db_path();
+ $db_name = $db_path ? basename( $db_path ) : '';
+ } else {
+ $db_bytes = $wpdb->get_var(
+ $wpdb->prepare(
+ 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s GROUP BY table_schema;',
+ DB_NAME
+ )
+ );
+ $db_name = DB_NAME;
+ }
// Add the database size to the list.
$rows[] = [
- 'Name' => DB_NAME,
+ 'Name' => $db_name,
'Size' => strtoupper( $db_bytes ) . $default_unit,
'Bytes' => strtoupper( $db_bytes ),
];
@@ -1142,6 +1247,10 @@ public function size( $args, $assoc_args ) {
$size_key = floor( log( (float) $row['Size'] ) / log( 1000 ) );
$sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
+ if ( is_infinite( $size_key ) ) {
+ $size_key = 0;
+ }
+
$size_format = isset( $sizes[ $size_key ] ) ? $sizes[ $size_key ] : $sizes[0];
}
@@ -1511,8 +1620,8 @@ public function search( $args, $assoc_args ) {
if ( ! $text_columns ) {
if ( $stats ) {
$skipped[] = $table;
- // Don't bother warning for term relationships (which is just 3 int columns).
- } elseif ( ! preg_match( '/_term_relationships$/', $table ) ) {
+ // Don't bother warning for term relationships (which is just 3 int columns) or SQLite.
+ } elseif ( ! preg_match( '/_term_relationships$/', $table ) && ! $this->is_sqlite() ) {
WP_CLI::warning( $primary_keys ? "No text columns for table '$table' - skipped." : "No primary key or text columns for table '$table' - skipped." );
}
continue;
@@ -1734,7 +1843,12 @@ public function columns( $args, $assoc_args ) {
);
$formatter_fields = [ 'Field', 'Type', 'Null', 'Key', 'Default', 'Extra' ];
- $formatter_args = [
+
+ if ( $this->is_sqlite() ) {
+ $formatter_fields = [ 'Field', 'Type', 'Null', 'Key', 'Default' ];
+ }
+
+ $formatter_args = [
'format' => $format,
];
diff --git a/src/DB_Command_SQLite.php b/src/DB_Command_SQLite.php
new file mode 100644
index 00000000..e4fe0878
--- /dev/null
+++ b/src/DB_Command_SQLite.php
@@ -0,0 +1,572 @@
+get_sqlite_db_path();
+
+ if ( ! $db_path ) {
+ return false;
+ }
+
+ try {
+ $pdo = new PDO( 'sqlite:' . $db_path );
+ $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
+ return $pdo;
+ } catch ( PDOException $e ) {
+ WP_CLI::debug( 'SQLite PDO connection failed: ' . $e->getMessage(), 'db' );
+ return false;
+ }
+ }
+
+ /**
+ * Create SQLite database.
+ */
+ protected function sqlite_create() {
+ $db_path = $this->get_sqlite_db_path();
+ if ( ! $db_path ) {
+ WP_CLI::error( 'Could not determine the database path.' );
+ }
+ $db_dir = dirname( $db_path );
+
+ // Create directory if it doesn't exist.
+ if ( ! is_dir( $db_dir ) ) {
+ if ( ! mkdir( $db_dir, 0755, true ) ) {
+ WP_CLI::error( "Could not create directory: {$db_dir}" );
+ }
+ }
+
+ // Check if database already exists.
+ if ( file_exists( $db_path ) ) {
+ WP_CLI::error( 'Database already exists.' );
+ }
+
+ // Create the SQLite database file.
+ try {
+ $pdo = new PDO( 'sqlite:' . $db_path );
+ $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
+ // Execute a simple query to initialize the database.
+ $pdo->exec( 'CREATE TABLE IF NOT EXISTS _wpcli_test (id INTEGER)' );
+ $pdo->exec( 'DROP TABLE _wpcli_test' );
+ } catch ( PDOException $e ) {
+ WP_CLI::error( 'Could not create SQLite database: ' . $e->getMessage() );
+ }
+
+ WP_CLI::success( 'Database created.' );
+ }
+
+ /**
+ * Drop SQLite database.
+ */
+ protected function sqlite_drop() {
+ $db_path = $this->get_sqlite_db_path();
+
+ if ( ! $db_path ) {
+ WP_CLI::error( 'Could not determine the database path.' );
+ }
+
+ if ( ! file_exists( $db_path ) ) {
+ WP_CLI::error( 'Database does not exist.' );
+ }
+
+ if ( ! unlink( $db_path ) ) {
+ WP_CLI::error( "Could not delete database file: {$db_path}" );
+ }
+
+ WP_CLI::success( 'Database dropped.' );
+ }
+
+ /**
+ * Reset SQLite database.
+ */
+ protected function sqlite_reset() {
+ $db_path = $this->get_sqlite_db_path();
+
+ if ( ! $db_path ) {
+ WP_CLI::error( 'Could not determine the database path.' );
+ }
+
+ // Delete if exists.
+ if ( file_exists( $db_path ) ) {
+ if ( ! unlink( $db_path ) ) {
+ WP_CLI::error( "Could not delete database file: {$db_path}" );
+ }
+ }
+
+ // Create directory if needed.
+ $db_dir = dirname( $db_path );
+ if ( ! is_dir( $db_dir ) ) {
+ if ( ! mkdir( $db_dir, 0755, true ) ) {
+ WP_CLI::error( "Could not create directory: {$db_dir}" );
+ }
+ }
+
+ // Recreate the SQLite database file.
+ try {
+ $pdo = new PDO( 'sqlite:' . $db_path );
+ $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
+ // Execute a simple query to initialize the database.
+ $pdo->exec( 'CREATE TABLE IF NOT EXISTS _wpcli_test (id INTEGER)' );
+ $pdo->exec( 'DROP TABLE _wpcli_test' );
+ } catch ( PDOException $e ) {
+ WP_CLI::error( 'Could not create SQLite database: ' . $e->getMessage() );
+ }
+
+ WP_CLI::success( 'Database reset.' );
+ }
+
+ /**
+ * Execute a query against the SQLite database.
+ *
+ * @param string $query SQL query to execute.
+ */
+ protected function sqlite_query( $query ) {
+ global $wpdb;
+
+ // Use $wpdb if the SQLite drop-in is loaded.
+ if ( isset( $wpdb ) && $wpdb instanceof \WP_SQLite_DB ) {
+ try {
+ $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE)\b/i', $query );
+
+ if ( $is_row_modifying_query ) {
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $affected_rows = $wpdb->query( $query );
+ if ( false === $affected_rows ) {
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
+ WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) );
+ }
+ WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" );
+ } else {
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $results = $wpdb->get_results( $query, ARRAY_A );
+
+ if ( $wpdb->last_error ) {
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
+ WP_CLI::error( 'Query failed: ' . strip_tags( $wpdb->last_error ) );
+ }
+
+ if ( empty( $results ) ) {
+ // No results to display.
+ return;
+ }
+
+ // Display as a table similar to MySQL output.
+ $headers = array_keys( $results[0] );
+ $this->display_table( $headers, $results );
+ }
+ } catch ( Exception $e ) {
+ WP_CLI::error( 'Query failed: ' . $e->getMessage() );
+ }
+ return;
+ }
+
+ // Fallback to PDO if the drop-in is not loaded.
+ $pdo = $this->get_sqlite_pdo();
+
+ if ( ! $pdo ) {
+ WP_CLI::error( 'Could not connect to SQLite database.' );
+ }
+
+ try {
+ $is_row_modifying_query = preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE)\b/i', $query );
+
+ if ( $is_row_modifying_query ) {
+ $stmt = $pdo->prepare( $query );
+ $stmt->execute();
+ $affected_rows = $stmt->rowCount();
+ WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" );
+ } else {
+ $stmt = $pdo->query( $query );
+
+ if ( ! $stmt ) {
+ // There was an error.
+ $error_info = $pdo->errorInfo();
+ WP_CLI::error( 'Query failed: ' . $error_info[2] );
+ }
+
+ // Fetch and display results.
+ $results = $stmt->fetchAll( PDO::FETCH_ASSOC );
+
+ if ( empty( $results ) ) {
+ // No results to display.
+ return;
+ }
+
+ // Display as a table similar to MySQL output.
+ $headers = array_keys( $results[0] );
+ $this->display_table( $headers, $results );
+ }
+ } catch ( PDOException $e ) {
+ WP_CLI::error( 'Query failed: ' . $e->getMessage() );
+ }
+ }
+
+ /**
+ * Display results as a table similar to MySQL output.
+ *
+ * @param array $headers Column headers.
+ * @param array $rows Data rows.
+ */
+ protected function display_table( $headers, $rows ) {
+ // Calculate column widths.
+ $widths = [];
+ foreach ( $headers as $header ) {
+ $widths[ $header ] = strlen( $header );
+ }
+
+ foreach ( $rows as $row ) {
+ foreach ( $row as $key => $value ) {
+ $widths[ $key ] = max( $widths[ $key ], strlen( (string) $value ) );
+ }
+ }
+
+ // Display header.
+ $separator = '+';
+ $header_line = '|';
+ foreach ( $headers as $header ) {
+ $separator .= str_repeat( '-', $widths[ $header ] + 2 ) . '+';
+ $header_line .= ' ' . str_pad( $header, $widths[ $header ] ) . ' |';
+ }
+
+ WP_CLI::line( $separator );
+ WP_CLI::line( $header_line );
+ WP_CLI::line( $separator );
+
+ // Display rows.
+ foreach ( $rows as $row ) {
+ $row_line = '|';
+ foreach ( $headers as $header ) {
+ $value = isset( $row[ $header ] ) ? $row[ $header ] : '';
+ $row_line .= ' ' . str_pad( (string) $value, $widths[ $header ] ) . ' |';
+ }
+ WP_CLI::line( $row_line );
+ }
+
+ WP_CLI::line( $separator );
+ }
+
+ /**
+ * Export SQLite database.
+ *
+ * @param string $file Output file path.
+ * @param array $assoc_args Associative arguments.
+ */
+ protected function sqlite_export( $file, $assoc_args ) {
+ $db_path = $this->get_sqlite_db_path();
+
+ if ( ! $db_path ) {
+ WP_CLI::error( 'Could not determine the database path.' );
+ }
+
+ if ( ! file_exists( $db_path ) ) {
+ WP_CLI::error( 'Database does not exist.' );
+ }
+
+ $pdo = $this->get_sqlite_pdo();
+ if ( ! $pdo ) {
+ WP_CLI::error( 'Could not connect to SQLite database.' );
+ }
+
+ $stdout = ( '-' === $file );
+
+ if ( $stdout ) {
+ $output = fopen( 'php://stdout', 'w' );
+ } else {
+ $output = fopen( $file, 'w' );
+ }
+
+ if ( ! $output ) {
+ WP_CLI::error( "Could not open file for writing: {$file}" );
+ }
+
+ $exclude_tables = Utils\get_flag_value( $assoc_args, 'exclude_tables', '' );
+ $exclude_tables = explode( ',', trim( $exclude_tables, ',' ) );
+ $exclude_tables = array_map( 'strtolower', $exclude_tables );
+
+ try {
+ // Export schema and data as SQL.
+ fwrite( $output, "-- SQLite database dump\n" );
+ fwrite( $output, '-- Database: ' . basename( $db_path ) . "\n\n" );
+
+ // Get all tables.
+ $stmt = $pdo->query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" );
+ if ( ! $stmt ) {
+ // There was an error.
+ $error_info = $pdo->errorInfo();
+ WP_CLI::error( 'Could not retrieve table list: ' . $error_info[2] );
+ }
+ $tables = $stmt->fetchAll( PDO::FETCH_COLUMN );
+
+ foreach ( $tables as $table ) {
+ if ( in_array( $table, $exclude_tables, true ) ) {
+ continue;
+ }
+
+ // Escape table name for identifiers.
+ $escaped_table = '"' . str_replace( '"', '""', $table ) . '"';
+
+ // Get CREATE TABLE statement.
+ $stmt = $pdo->query( "SELECT sql FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote( $table ) );
+ if ( ! $stmt ) {
+ // There was an error.
+ $error_info = $pdo->errorInfo();
+ WP_CLI::error( "Could not retrieve CREATE TABLE statement for table {$escaped_table}: " . $error_info[2] );
+ }
+ $create_stmt = $stmt->fetchColumn();
+
+ if ( isset( $assoc_args['add-drop-table'] ) ) {
+ fwrite( $output, "DROP TABLE IF EXISTS {$escaped_table};\n" );
+ }
+
+ fwrite( $output, $create_stmt . ";\n\n" );
+
+ // Export data.
+ $stmt = $pdo->query( "SELECT * FROM {$escaped_table}" );
+ if ( ! $stmt ) {
+ // There was an error.
+ $error_info = $pdo->errorInfo();
+ WP_CLI::error( "Could not retrieve data for table {$escaped_table}: " . $error_info[2] );
+ }
+ $rows = $stmt->fetchAll( PDO::FETCH_ASSOC );
+
+ foreach ( $rows as $row ) {
+ $columns = array_keys( $row );
+ $values = array_map( [ $pdo, 'quote' ], array_values( $row ) );
+
+ fwrite( $output, "INSERT INTO {$escaped_table} (" . implode( ', ', $columns ) . ') VALUES (' . implode( ', ', $values ) . ");\n" );
+ }
+
+ fwrite( $output, "\n" );
+ }
+
+ fwrite( $output, '-- Dump completed on ' . gmdate( 'Y-m-d H:i:s' ) . "\n\n" );
+ } catch ( PDOException $e ) {
+ fclose( $output );
+ WP_CLI::error( 'Export failed: ' . $e->getMessage() );
+ }
+
+ fclose( $output );
+
+ if ( ! $stdout ) {
+ if ( isset( $assoc_args['porcelain'] ) ) {
+ WP_CLI::line( $file );
+ } else {
+ WP_CLI::success( "Exported to '{$file}'." );
+ }
+ }
+ }
+
+ /**
+ * Import SQL into SQLite database.
+ *
+ * @param string $file Input file path.
+ */
+ protected function sqlite_import( $file ) {
+ $pdo = $this->get_sqlite_pdo();
+
+ if ( ! $pdo ) {
+ WP_CLI::error( 'Could not connect to SQLite database.' );
+ }
+
+ if ( '-' === $file ) {
+ $sql = stream_get_contents( STDIN );
+ $file = 'STDIN';
+ } else {
+ if ( ! is_readable( $file ) ) {
+ WP_CLI::error( sprintf( 'Import file missing or not readable: %s', $file ) );
+ }
+ $sql = file_get_contents( $file );
+ }
+
+ if ( false === $sql ) {
+ WP_CLI::error( 'Could not read import file.' );
+ }
+
+ try {
+ // Split SQL into individual statements.
+ $lines = preg_split( '/;[\r\n]+/', $sql );
+ if ( ! is_array( $lines ) ) {
+ $lines = [];
+ }
+ $statements = array_filter(
+ array_map(
+ 'trim',
+ $lines
+ )
+ );
+
+ $pdo->beginTransaction();
+
+ foreach ( $statements as $statement ) {
+ if ( empty( $statement ) || 0 === strpos( $statement, '--' ) ) {
+ continue;
+ }
+
+ $pdo->exec( $statement );
+ }
+
+ $pdo->commit();
+
+ WP_CLI::success( sprintf( "Imported from '%s'.", $file ) );
+ } catch ( PDOException $e ) {
+ $pdo->rollBack();
+ WP_CLI::error( 'Import failed: ' . $e->getMessage() );
+ }
+ }
+
+ /**
+ * Get SQLite database size.
+ *
+ * @return int Database file size in bytes, or 0 if not found.
+ */
+ protected function sqlite_size() {
+ $db_path = $this->get_sqlite_db_path();
+
+ if ( ! $db_path || ! file_exists( $db_path ) ) {
+ return 0;
+ }
+
+ $size = filesize( $db_path );
+ if ( false === $size ) {
+ return 0;
+ }
+
+ return $size;
+ }
+
+ /**
+ * Load WordPress db.php drop-in if SQLite is detected.
+ *
+ * This should be called early in commands that run at after_wp_config_load.
+ */
+ protected function maybe_load_sqlite_dropin() {
+ if ( ! $this->is_sqlite() ) {
+ return;
+ }
+
+ // Check if already loaded.
+ if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) ) {
+ return;
+ }
+
+ $wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content';
+ $db_dropin_path = $wp_content_dir . '/db.php';
+
+ if ( ! file_exists( $db_dropin_path ) ) {
+ return;
+ }
+
+ // Constants used in wp-includes/functions.php
+ if ( ! defined( 'WPINC' ) ) {
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
+ define( 'WPINC', 'wp-includes' );
+ }
+
+ if ( ! defined( 'WP_CONTENT_DIR' ) ) {
+ define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' );
+ }
+
+ // Load required WordPress files if not already loaded.
+ if ( ! function_exists( 'add_action' ) ) {
+ $required_files = [
+ ABSPATH . WPINC . '/compat.php',
+ ABSPATH . WPINC . '/plugin.php',
+ // Defines `wp_debug_backtrace_summary()` as used by wpdb.
+ ABSPATH . WPINC . '/functions.php',
+ ABSPATH . WPINC . '/class-wpdb.php',
+ ];
+
+ foreach ( $required_files as $required_file ) {
+ if ( file_exists( $required_file ) ) {
+ require_once $required_file;
+ }
+ }
+ }
+
+ // Load the db.php drop-in.
+ require_once $db_dropin_path;
+ }
+}
diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php
index 80950306..92b1f46c 100644
--- a/tests/phpstan/scan-files.php
+++ b/tests/phpstan/scan-files.php
@@ -7,4 +7,7 @@
define( 'DB_PASSWORD', '' );
define( 'DB_CHARSET', '' );
define( 'DB_COLLATE', '' );
+
+ class WP_SQLite_DB extends \wpdb {
+ }
}