Skip to content

Commit 13c8a34

Browse files
adamzielclaude
andcommitted
Test zero dates in SELECT and fix YEAR/MONTH/DAY for zero dates
Add tests verifying that stored zero dates and zero-in-dates can be selected, compared, ordered, and filtered – matching MySQL behavior. Fix YEAR(), MONTH(), and DAY() functions to return 0 for zero date parts. Previously, strtotime() couldn't parse dates like '0000-00-00' or '2020-00-15', producing wrong results. Now the date parts are extracted directly from the string when possible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cc3c494 commit 13c8a34

2 files changed

Lines changed: 102 additions & 11 deletions

File tree

tests/WP_SQLite_Driver_Tests.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2586,6 +2586,91 @@ public function testZeroInDateInUpdateRejectedWhenNoZeroInDateAndStrictModeAreOn
25862586
);
25872587
}
25882588

2589+
/**
2590+
* Test that stored zero dates can be selected and compared.
2591+
*
2592+
* In MySQL, zero dates are regular values for reads — they can appear in
2593+
* WHERE, ORDER BY, and comparisons regardless of the current SQL mode.
2594+
*/
2595+
public function testSelectZeroDatesComparison() {
2596+
$this->assertQuery( "SET sql_mode = 'STRICT_TRANS_TABLES'" );
2597+
2598+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('zero', '0000-00-00 00:00:00');" );
2599+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('real', '2022-01-15 14:30:00');" );
2600+
2601+
// Zero dates compare as less than real dates.
2602+
$this->assertQuery( "SELECT option_name FROM _dates WHERE option_value < '2000-01-01 00:00:00' ORDER BY option_value;" );
2603+
$results = $this->engine->get_query_results();
2604+
$this->assertCount( 1, $results );
2605+
$this->assertEquals( 'zero', $results[0]->option_name );
2606+
2607+
// Equality match on zero date.
2608+
$this->assertQuery( "SELECT option_name FROM _dates WHERE option_value = '0000-00-00 00:00:00';" );
2609+
$results = $this->engine->get_query_results();
2610+
$this->assertCount( 1, $results );
2611+
$this->assertEquals( 'zero', $results[0]->option_name );
2612+
}
2613+
2614+
/**
2615+
* Test ORDER BY with a mix of zero and non-zero dates.
2616+
*/
2617+
public function testSelectZeroDatesOrderBy() {
2618+
$this->assertQuery( "SET sql_mode = 'STRICT_TRANS_TABLES'" );
2619+
2620+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('b', '2022-06-01 00:00:00');" );
2621+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('a', '0000-00-00 00:00:00');" );
2622+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('c', '2023-01-01 00:00:00');" );
2623+
2624+
$this->assertQuery( 'SELECT option_name FROM _dates ORDER BY option_value ASC;' );
2625+
$results = $this->engine->get_query_results();
2626+
$this->assertCount( 3, $results );
2627+
$this->assertEquals( 'a', $results[0]->option_name );
2628+
$this->assertEquals( 'b', $results[1]->option_name );
2629+
$this->assertEquals( 'c', $results[2]->option_name );
2630+
}
2631+
2632+
/**
2633+
* Test that zero-in-dates stored in the database can be read back
2634+
* and filtered in SELECT statements.
2635+
*/
2636+
public function testSelectZeroInDates() {
2637+
$this->assertQuery( "SET sql_mode = 'STRICT_TRANS_TABLES'" );
2638+
2639+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('zero-month', '2020-00-15 00:00:00');" );
2640+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('zero-day', '2020-01-00 00:00:00');" );
2641+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('normal', '2020-01-15 00:00:00');" );
2642+
2643+
// All three rows are readable.
2644+
$this->assertQuery( 'SELECT option_name, option_value FROM _dates ORDER BY option_value ASC;' );
2645+
$results = $this->engine->get_query_results();
2646+
$this->assertCount( 3, $results );
2647+
$this->assertEquals( '2020-00-15 00:00:00', $results[0]->option_value );
2648+
$this->assertEquals( '2020-01-00 00:00:00', $results[1]->option_value );
2649+
$this->assertEquals( '2020-01-15 00:00:00', $results[2]->option_value );
2650+
2651+
// Filtering by a zero-in-date value works.
2652+
$this->assertQuery( "SELECT option_name FROM _dates WHERE option_value = '2020-00-15 00:00:00';" );
2653+
$results = $this->engine->get_query_results();
2654+
$this->assertCount( 1, $results );
2655+
$this->assertEquals( 'zero-month', $results[0]->option_name );
2656+
}
2657+
2658+
/**
2659+
* Test date functions on zero dates — YEAR(), MONTH(), DAY() all return 0.
2660+
*/
2661+
public function testDateFunctionsOnZeroDates() {
2662+
$this->assertQuery( "SET sql_mode = 'STRICT_TRANS_TABLES'" );
2663+
2664+
$this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('zero', '0000-00-00 00:00:00');" );
2665+
2666+
$this->assertQuery( "SELECT YEAR(option_value) as y, MONTH(option_value) as m, DAY(option_value) as d FROM _dates;" );
2667+
$results = $this->engine->get_query_results();
2668+
$this->assertCount( 1, $results );
2669+
$this->assertEquals( 0, $results[0]->y );
2670+
$this->assertEquals( 0, $results[0]->m );
2671+
$this->assertEquals( 0, $results[0]->d );
2672+
}
2673+
25892674
public function testCaseInsensitiveSelect() {
25902675
$this->assertQuery(
25912676
"CREATE TABLE _tmp_table (

wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,13 @@ public function dateformat( $date, $format ) {
239239
*/
240240
public function month( $field ) {
241241
/*
242-
* From https://www.php.net/manual/en/datetime.format.php:
243-
*
244-
* n - Numeric representation of a month, without leading zeros.
245-
* 1 through 12
242+
* MySQL returns 0 for MONTH('0000-00-00') and for dates with
243+
* zero month parts like '2020-00-15'. PHP's strtotime() can't
244+
* parse these, so we extract the month directly from the string.
246245
*/
246+
if ( preg_match( '/\d{4}-(\d{2})/', $field, $matches ) ) {
247+
return intval( $matches[1] );
248+
}
247249
return intval( gmdate( 'n', strtotime( $field ) ) );
248250
}
249251

@@ -256,10 +258,12 @@ public function month( $field ) {
256258
*/
257259
public function year( $field ) {
258260
/*
259-
* From https://www.php.net/manual/en/datetime.format.php:
260-
*
261-
* Y - A full numeric representation of a year, 4 digits.
261+
* MySQL returns 0 for YEAR('0000-00-00'). PHP's strtotime()
262+
* can't parse zero dates, so we extract the year directly.
262263
*/
264+
if ( preg_match( '/(\d{4})-\d{2}/', $field, $matches ) ) {
265+
return intval( $matches[1] );
266+
}
263267
return intval( gmdate( 'Y', strtotime( $field ) ) );
264268
}
265269

@@ -272,11 +276,13 @@ public function year( $field ) {
272276
*/
273277
public function day( $field ) {
274278
/*
275-
* From https://www.php.net/manual/en/datetime.format.php:
276-
*
277-
* j - Day of the month without leading zeros.
278-
* 1 to 31.
279+
* MySQL returns 0 for DAY('0000-00-00') and for dates with
280+
* zero day parts like '2020-01-00'. PHP's strtotime() can't
281+
* parse these, so we extract the day directly from the string.
279282
*/
283+
if ( preg_match( '/\d{4}-\d{2}-(\d{2})/', $field, $matches ) ) {
284+
return intval( $matches[1] );
285+
}
280286
return intval( gmdate( 'j', strtotime( $field ) ) );
281287
}
282288

0 commit comments

Comments
 (0)