diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php index 06218632c..57d72f70c 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php @@ -330,6 +330,23 @@ class WP_MySQL_On_SQLite extends PDO { '%y' => '%y', ); + /** + * Map MySQL INTERVAL units to SQLite units and value multipliers. + * + * MICROSECOND is excluded because DATETIME() drops fractional seconds. + * Composite units, such as HOUR_MINUTE, are not supported. + */ + private const INTERVAL_UNIT_MAP = array( + WP_MySQL_Lexer::SECOND_SYMBOL => array( 'SECOND', 1 ), + WP_MySQL_Lexer::MINUTE_SYMBOL => array( 'MINUTE', 1 ), + WP_MySQL_Lexer::HOUR_SYMBOL => array( 'HOUR', 1 ), + WP_MySQL_Lexer::DAY_SYMBOL => array( 'DAY', 1 ), + WP_MySQL_Lexer::WEEK_SYMBOL => array( 'DAY', 7 ), + WP_MySQL_Lexer::MONTH_SYMBOL => array( 'MONTH', 1 ), + WP_MySQL_Lexer::QUARTER_SYMBOL => array( 'MONTH', 3 ), + WP_MySQL_Lexer::YEAR_SYMBOL => array( 'YEAR', 1 ), + ); + /** * A map of MySQL data types to implicit default values for non-strict mode. * @@ -4349,6 +4366,8 @@ private function translate( $node ): ?string { return null; } return $this->translate_sequence( $node->get_children() ); + case 'bitExpr': + return $this->translate_bit_expr( $node ); case 'simpleExprBody': return $this->translate_simple_expr_body( $node ); case 'predicateOperations': @@ -4915,9 +4934,86 @@ private function translate_simple_expr_body( WP_Parser_Node $node ): string { } } + /* + * Reject "INTERVAL n unit + expr": the parser includes following AND, OR, + * and XOR conditions in "expr", unlike MySQL. It also reads + * "expr + INTERVAL 1 DAY + x" as "expr + (INTERVAL 1 DAY + x)". + * + * TODO: Support prefix intervals after switching to the LALR(1) parser. + */ + if ( null !== $token && WP_MySQL_Lexer::INTERVAL_SYMBOL === $token->id ) { + throw $this->new_not_supported_exception( + 'INTERVAL n unit followed by "+"; use "(expr + INTERVAL n unit)" instead' + ); + } + return $this->translate_sequence( $node->get_children() ); } + /** + * Translate a "bitExpr" AST node. + * + * Translate interval additions and subtractions to a single date function + * call, applying each interval from left to right, as MySQL does. + * + * TODO: After switching to LALR(1), use the tree's operator grouping + * and remove the loop over this parser's flat interval sequence. + * + * @param WP_Parser_Node $node The "bitExpr" AST node. + * @return string|null The translated value. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. + */ + private function translate_bit_expr( WP_Parser_Node $node ): ?string { + /* + * The first child is an expression. Each interval adds four children: + * operator, INTERVAL, value, unit. A final "op bitExpr" may follow. + */ + $children = $node->get_children(); + $count = count( $children ); + $sql = $this->translate( $children[0] ); + $intervals = array(); + $i = 1; + while ( + isset( $children[ $i + 1 ] ) + && $children[ $i + 1 ] instanceof WP_Parser_Token + && WP_MySQL_Lexer::INTERVAL_SYMBOL === $children[ $i + 1 ]->id + ) { + $intervals[] = array( + WP_MySQL_Lexer::MINUS_OPERATOR === $children[ $i ]->id, + $children[ $i + 2 ], + $children[ $i + 3 ], + ); + $i += 4; + } + if ( count( $intervals ) > 0 ) { + $sql = $this->translate_interval_arithmetic( $sql, $intervals ); + } + + if ( $i === $count ) { + return $sql; + } + + /* + * The parser groups "a + b + INTERVAL 1 DAY" as "a + (b + INTERVAL 1 DAY)". + * MySQL applies the interval to "a + b". Reject this form to avoid + * applying the interval to only part of the expression. For simplicity, + * also reject operators that bind less tightly than "+", such as "|". + * + * TODO: Remove this guard and support mixed arithmetic with LALR(1). + */ + $operator = $children[ $i ]; + $operand = $children[ $i + 1 ]; + if ( $operand->has_child_token( WP_MySQL_Lexer::INTERVAL_SYMBOL ) ) { + throw $this->new_not_supported_exception( + sprintf( + 'INTERVAL after the "%s" operator; add parentheses to make the grouping explicit', + $operator->get_value() + ) + ); + } + return sprintf( '%s %s %s', $sql, $this->translate( $operator ), $this->translate( $operand ) ); + } + /** * Translate a MySQL CAST expression to SQLite. * @@ -5051,18 +5147,9 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string case WP_MySQL_Lexer::DATE_ADD_SYMBOL: case WP_MySQL_Lexer::DATE_SUB_SYMBOL: $nodes = $node->get_child_nodes(); - $value = $this->translate( $nodes[1] ); - $unit = $this->translate( $nodes[2] ); - if ( 'WEEK' === $unit ) { - $unit = 'DAY'; - $value = 7 * $value; - } - return sprintf( - "DATETIME(%s, '%s' || %s || ' %s')", + return $this->translate_interval_arithmetic( $this->translate( $nodes[0] ), - WP_MySQL_Lexer::DATE_SUB_SYMBOL === $child->id ? '-' : '+', - $value, - $unit + array( array( WP_MySQL_Lexer::DATE_SUB_SYMBOL === $child->id, $nodes[1], $nodes[2] ) ) ); case WP_MySQL_Lexer::LEFT_SYMBOL: $nodes = $node->get_child_nodes(); @@ -5076,6 +5163,62 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string } } + /** + * Translate MySQL date arithmetic to a SQLite DATE() or DATETIME() call. + * + * Each interval becomes a modifier, and SQLite applies them from left to right. + * Parenthesize the value before appending the unit: SQLite evaluates "||" + * before "*". Negate the value for subtraction so negative intervals work + * without invalid sign combinations such as "+-5 HOUR". + * Convert units where needed, e.g., WEEK to 7 days. + * + * MySQL returns a date when it adds only date units to a date. The driver + * stores dates as 10-character strings, so use DATE() for such inputs. + * + * @param string $expr The translated date expression. + * @param array $intervals List of intervals, each as an array of: + * - bool Whether the interval is subtracted. + * - WP_Parser_Node The interval value expression node. + * - WP_Parser_Node The "interval" AST node holding the unit. + * @return string The translated value. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. + */ + private function translate_interval_arithmetic( string $expr, array $intervals ): string { + $modifiers = array(); + $date_units = true; + foreach ( $intervals as list( $subtract, $value, $interval ) ) { + // Token IDs also cover unit synonyms, e.g., SQL_TSI_HOUR. + $unit_token = $interval->get_first_descendant_token(); + if ( ! isset( self::INTERVAL_UNIT_MAP[ $unit_token->id ] ) ) { + throw $this->new_not_supported_exception( + sprintf( 'INTERVAL unit: %s', $unit_token->get_value() ) + ); + } + + list( $unit, $multiplier ) = self::INTERVAL_UNIT_MAP[ $unit_token->id ]; + + $value_sql = sprintf( '(%s)', $this->translate( $value ) ); + if ( 1 !== $multiplier ) { + $value_sql = sprintf( '(%s * %d)', $value_sql, $multiplier ); + } + if ( $subtract ) { + $value_sql = '-' . $value_sql; + } + $modifiers[] = sprintf( "%s || ' %s'", $value_sql, $unit ); + $date_units = $date_units && in_array( $unit, array( 'DAY', 'MONTH', 'YEAR' ), true ); + } + + $modifiers_sql = implode( ', ', $modifiers ); + if ( ! $date_units ) { + return sprintf( 'DATETIME(%s, %s)', $expr, $modifiers_sql ); + } + return sprintf( + 'CASE WHEN LENGTH(%1$s) = 10 THEN DATE(%1$s, %2$s) ELSE DATETIME(%1$s, %2$s) END', + $expr, + $modifiers_sql + ); + } + /** * Translate a MySQL function call to SQLite. * diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php index 659d38b1d..d404a0b44 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php @@ -932,6 +932,275 @@ public function testDateSubFunction() { $this->assertEquals( '2007-01-02 13:29:17', $result[0]->output ); } + public function testDateAddFunctionWithNegativeAndComputedIntervals() { + // Expected values were taken from MySQL 8.4. + $result = $this->assertQuery( + "SELECT DATE_ADD('2024-01-01 10:00:00', INTERVAL -5 HOUR) as output" + ); + $this->assertEquals( '2024-01-01 05:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT DATE_SUB('2024-01-01 10:00:00', INTERVAL -5 HOUR) as output" + ); + $this->assertEquals( '2024-01-01 15:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT DATE_SUB('2024-01-01 10:00:00', INTERVAL 2 * 3 HOUR) as output" + ); + $this->assertEquals( '2024-01-01 04:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT DATE_ADD('2024-01-01 10:00:00', INTERVAL 1 week) as output" + ); + $this->assertEquals( '2024-01-08 10:00:00', $result[0]->output ); + } + + public function testIntervalArithmetic() { + // Expected values were taken from MySQL 8.4. + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' - INTERVAL 5 HOUR as output" + ); + $this->assertEquals( '2024-01-01 05:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 5 HOUR as output" + ); + $this->assertEquals( '2024-01-01 15:00:00', $result[0]->output ); + + // Intervals chain from left to right. + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 5 HOUR + INTERVAL 1 DAY as output" + ); + $this->assertEquals( '2024-01-02 15:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 1 DAY - INTERVAL 5 HOUR as output" + ); + $this->assertEquals( '2024-01-02 05:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 1 WEEK as output" + ); + $this->assertEquals( '2024-01-08 10:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 1 hour as output" + ); + $this->assertEquals( '2024-01-01 11:00:00', $result[0]->output ); + + // The value is an expression, and may be negative or a string. + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' - INTERVAL 2 * 3 HOUR as output" + ); + $this->assertEquals( '2024-01-01 04:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL -5 HOUR as output" + ); + $this->assertEquals( '2024-01-01 05:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL '5' HOUR as output" + ); + $this->assertEquals( '2024-01-01 15:00:00', $result[0]->output ); + + // NULL on either side yields NULL. + $result = $this->assertQuery( 'SELECT NULL + INTERVAL 1 DAY as output' ); + $this->assertNull( $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL NULL DAY as output" + ); + $this->assertNull( $result[0]->output ); + + // The case from the original report. + $result = $this->assertQuery( + "SELECT DATE('2024-01-01 10:00:00' - INTERVAL 5 HOUR) as output" + ); + $this->assertEquals( '2024-01-01', $result[0]->output ); + } + + public function testIntervalArithmeticOnColumns() { + $this->assertQuery( 'CREATE TABLE _events (id INT, occurred_at DATETIME)' ); + $this->assertQuery( + "INSERT INTO _events VALUES (1, '2024-01-01 10:00:00'), (2, '2024-01-03 10:00:00'), (3, NULL)" + ); + + // Expected values were taken from MySQL 8.4. + $result = $this->assertQuery( + 'SELECT id, occurred_at - INTERVAL 1 DAY as output FROM _events ORDER BY id' + ); + $this->assertEquals( '2023-12-31 10:00:00', $result[0]->output ); + $this->assertEquals( '2024-01-02 10:00:00', $result[1]->output ); + $this->assertNull( $result[2]->output ); + + $result = $this->assertQuery( + "SELECT id FROM _events WHERE occurred_at > '2024-01-02 00:00:00' - INTERVAL 1 DAY ORDER BY id" + ); + $this->assertEquals( array( 1, 2 ), array_map( 'intval', array_column( $result, 'id' ) ) ); + + // Conditions after the interval are not part of it. + $this->assertQuery( + "DELETE FROM _events WHERE occurred_at < '2024-01-03 00:00:00' - INTERVAL 1 DAY AND id = 99" + ); + $result = $this->assertQuery( 'SELECT COUNT(*) as count FROM _events' ); + $this->assertEquals( 3, $result[0]->count ); + } + + public function testIntervalArithmeticOnDates() { + // Expected values were taken from MySQL 8.4. + // Adding only date units to a date returns a date. + $result = $this->assertQuery( "SELECT '2024-01-01' + INTERVAL 1 DAY as output" ); + $this->assertEquals( '2024-01-02', $result[0]->output ); + + $result = $this->assertQuery( "SELECT '2024-01-01' + INTERVAL 1 DAY + INTERVAL 1 MONTH as output" ); + $this->assertEquals( '2024-02-02', $result[0]->output ); + + $result = $this->assertQuery( "SELECT '2024-01-01' - INTERVAL 1 YEAR as output" ); + $this->assertEquals( '2023-01-01', $result[0]->output ); + + $result = $this->assertQuery( "SELECT DATE_ADD('2024-01-01', INTERVAL 1 WEEK) as output" ); + $this->assertEquals( '2024-01-08', $result[0]->output ); + + $result = $this->assertQuery( "SELECT DATE_SUB('2024-01-01', INTERVAL 1 QUARTER) as output" ); + $this->assertEquals( '2023-10-01', $result[0]->output ); + + // Time units and datetime inputs return a datetime. + $result = $this->assertQuery( "SELECT '2024-01-01' + INTERVAL 30 SECOND as output" ); + $this->assertEquals( '2024-01-01 00:00:30', $result[0]->output ); + + $result = $this->assertQuery( "SELECT '2024-01-01' + INTERVAL 1 DAY - INTERVAL 1 HOUR as output" ); + $this->assertEquals( '2024-01-01 23:00:00', $result[0]->output ); + + $result = $this->assertQuery( "SELECT '2024-01-01 00:00:00' + INTERVAL 1 DAY as output" ); + $this->assertEquals( '2024-01-02 00:00:00', $result[0]->output ); + + $this->assertQuery( 'CREATE TABLE _events (id INT, d DATE, dt DATETIME)' ); + $this->assertQuery( + "INSERT INTO _events VALUES (1, '2024-01-01', '2024-01-01 00:00:00'), (2, '2024-01-02', '2024-01-02 00:00:00'), (3, '2024-01-03', '2024-01-03 00:00:00')" + ); + + $result = $this->assertQuery( + 'SELECT d + INTERVAL 1 DAY as date_output, dt + INTERVAL 1 DAY as datetime_output, d + INTERVAL 1 HOUR as hour_output FROM _events WHERE id = 1' + ); + $this->assertEquals( '2024-01-02', $result[0]->date_output ); + $this->assertEquals( '2024-01-02 00:00:00', $result[0]->datetime_output ); + $this->assertEquals( '2024-01-01 01:00:00', $result[0]->hour_output ); + + // Comparisons with dates match the same rows as in MySQL. + $result = $this->assertQuery( "SELECT id FROM _events WHERE d = '2024-01-03' - INTERVAL 1 DAY" ); + $this->assertEquals( array( 2 ), array_map( 'intval', array_column( $result, 'id' ) ) ); + + $result = $this->assertQuery( "SELECT COUNT(*) as count FROM _events WHERE DATE_ADD(d, INTERVAL 7 DAY) = '2024-01-08'" ); + $this->assertEquals( 1, $result[0]->count ); + + $this->assertQuery( "DELETE FROM _events WHERE d < '2024-01-03' - INTERVAL 1 DAY" ); + $result = $this->assertQuery( 'SELECT id FROM _events ORDER BY id' ); + $this->assertEquals( array( 2, 3 ), array_map( 'intval', array_column( $result, 'id' ) ) ); + } + + public function testIntervalArithmeticPrefixForm() { + // TODO: With LALR(1), assert MySQL results instead of expecting errors. + // Keep the DELETE regression check to verify that unrelated rows survive. + $this->assertQuery( 'CREATE TABLE _events (id INT, occurred_at DATETIME)' ); + $this->assertQuery( + "INSERT INTO _events VALUES (1, '2024-01-01 10:00:00'), (2, '2024-01-03 10:00:00')" + ); + + $error = 'MySQL query not supported. Cause: INTERVAL n unit followed by "+"; use "(expr + INTERVAL n unit)" instead'; + $this->assertQueryError( "SELECT INTERVAL 5 HOUR + '2024-01-01 10:00:00'", $error ); + $this->assertQueryError( + "DELETE FROM _events WHERE INTERVAL 30 DAY + occurred_at < '2025-01-01' AND id = 99", + $error + ); + + // The parser treats a suffix interval followed by "+" as a prefix interval. + $this->assertQueryError( "SELECT '2024-01-01 10:00:00' + INTERVAL 1 DAY + 0", $error ); + $this->assertQueryError( "SELECT '2024-01-01 10:00:00' - INTERVAL 1 DAY + 0", $error ); + + $result = $this->assertQuery( 'SELECT COUNT(*) as count FROM _events' ); + $this->assertEquals( 2, $result[0]->count ); + } + + public function testIntervalArithmeticOnPartialOperand() { + // TODO: With LALR(1), replace these rejection tests with MySQL result checks. + // The parser applies the interval to the date alone. + // MySQL applies it to the sum, so reject the unparenthesized expression. + $this->assertQueryError( + "SELECT 1 + '2024-01-01' + INTERVAL 1 DAY", + 'MySQL query not supported. Cause: INTERVAL after the "+" operator; add parentheses to make the grouping explicit' + ); + + $this->assertQueryError( + "SELECT 2 * '2024-01-01' - INTERVAL 1 DAY", + 'MySQL query not supported. Cause: INTERVAL after the "*" operator; add parentheses to make the grouping explicit' + ); + + // MySQL groups "|" like the parser, but it is rejected for simplicity. + $this->assertQueryError( + "SELECT 0 | '2024-01-01' + INTERVAL 1 DAY", + 'MySQL query not supported. Cause: INTERVAL after the "|" operator; add parentheses to make the grouping explicit' + ); + } + + public function testIntervalArithmeticUnits() { + $this->assertQuery( 'CREATE TABLE _events (id INT, occurred_at DATETIME)' ); + $this->assertQuery( + "INSERT INTO _events VALUES (1, '2024-01-01 10:00:00'), (2, '2024-01-03 10:00:00'), (3, NULL)" + ); + + // Expected values were taken from MySQL 8.4. + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 1 QUARTER as output" + ); + $this->assertEquals( '2024-04-01 10:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 2 * 2 QUARTER as output" + ); + $this->assertEquals( '2025-01-01 10:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT DATE_SUB('2024-01-01 10:00:00', INTERVAL -2 QUARTER) as output" + ); + $this->assertEquals( '2024-07-01 10:00:00', $result[0]->output ); + + // The SQL_TSI_* units are synonyms for the plain units. + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' + INTERVAL 1 SQL_TSI_HOUR as output" + ); + $this->assertEquals( '2024-01-01 11:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT '2024-01-01 10:00:00' - INTERVAL 1 SQL_TSI_QUARTER as output" + ); + $this->assertEquals( '2023-10-01 10:00:00', $result[0]->output ); + + $result = $this->assertQuery( + "SELECT DATE_ADD('2024-01-01 10:00:00', INTERVAL 2 SQL_TSI_WEEK) as output" + ); + $this->assertEquals( '2024-01-15 10:00:00', $result[0]->output ); + + $this->assertQuery( 'UPDATE _events SET occurred_at = occurred_at + INTERVAL 1 QUARTER' ); + $result = $this->assertQuery( 'SELECT occurred_at FROM _events ORDER BY id' ); + $this->assertEquals( '2024-04-01 10:00:00', $result[0]->occurred_at ); + $this->assertEquals( '2024-04-03 10:00:00', $result[1]->occurred_at ); + $this->assertNull( $result[2]->occurred_at ); + } + + public function testIntervalArithmeticWithUnsupportedUnits() { + // SQLite DATETIME() drops fractional seconds. + $this->assertQueryError( + "SELECT '2024-01-01 10:00:00' + INTERVAL 1 MICROSECOND", + 'MySQL query not supported. Cause: INTERVAL unit: MICROSECOND' + ); + + $this->assertQueryError( + "SELECT DATE_ADD('2024-01-01 10:00:00', INTERVAL '1:30' HOUR_MINUTE)", + 'MySQL query not supported. Cause: INTERVAL unit: HOUR_MINUTE' + ); + } + public function testLeftFunction1Char() { $result = $this->assertQuery( 'SELECT LEFT("abc", 1) as output' @@ -13508,7 +13777,10 @@ public function testCreateTableWithDefaultExpressions(): void { $result = $this->sqlite->query( 'PRAGMA table_info(t)' )->fetchAll(); $this->assertSame( null, $result[0]['dflt_value'] ); $this->assertSame( '1 + 2', $result[1]['dflt_value'] ); - $this->assertSame( "DATETIME(CURRENT_TIMESTAMP, '+' || 1 || ' YEAR')", $result[2]['dflt_value'] ); + $this->assertSame( + "CASE WHEN LENGTH(CURRENT_TIMESTAMP) = 10 THEN DATE(CURRENT_TIMESTAMP, (1) || ' YEAR') ELSE DATETIME(CURRENT_TIMESTAMP, (1) || ' YEAR') END", + $result[2]['dflt_value'] + ); $this->assertSame( "('a' || 'b')", $result[3]['dflt_value'] ); } diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php index d6720e5b6..a09a0dcdb 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Translation_Tests.php @@ -1735,6 +1735,34 @@ public function testConcatFunction(): void { ); } + public function testIntervalArithmeticInParentheses(): void { + // TODO: With LALR(1), also test these expressions without parentheses. + // Parentheses prevent the parser from treating this as a prefix interval. + $this->assertQuery( + "SELECT ( DATETIME('2024-01-01', (1) || ' HOUR') ) + 0 AS `a`", + "SELECT ('2024-01-01' + INTERVAL 1 HOUR) + 0 AS a" + ); + + // Parentheses make the interval apply to the whole expression. + $this->assertQuery( + "SELECT DATETIME(( 1 + '2024-01-01' ), (1) || ' HOUR') AS `a`", + "SELECT (1 + '2024-01-01') + INTERVAL 1 HOUR AS a" + ); + } + + public function testIntervalArithmeticChains(): void { + // Only date units return a date for date inputs, which have 10 characters. + $this->assertQuery( + "SELECT CASE WHEN LENGTH('2024-01-01') = 10 THEN DATE('2024-01-01', (1) || ' DAY', -(2) || ' MONTH') ELSE DATETIME('2024-01-01', (1) || ' DAY', -(2) || ' MONTH') END AS `a`", + "SELECT '2024-01-01' + INTERVAL 1 DAY - INTERVAL 2 MONTH AS a" + ); + + $this->assertQuery( + "SELECT DATETIME('2024-01-01', (1) || ' DAY', (2) || ' HOUR') AS `a`", + "SELECT '2024-01-01' + INTERVAL 1 DAY + INTERVAL 2 HOUR AS a" + ); + } + public function testIndexHints(): void { // USE INDEX $this->assertQuery(