Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7908,6 +7908,14 @@ private function convert_information_schema_exception( WP_SQLite_Information_Sch
null,
array( '42000', 1072, $driver_message )
);
case WP_SQLite_Information_Schema_Exception::TYPE_INVALID_DEFAULT_VALUE:
$driver_message = sprintf( "Invalid default value for '%s'", $e->get_data()['column_name'] );
return $this->new_driver_exception(
'SQLSTATE[42000]: Syntax error or access violation: 1067 ' . $driver_message,
'42000',
null,
array( '42000', 1067, $driver_message )
);
case WP_SQLite_Information_Schema_Exception::TYPE_CONSTRAINT_DOES_NOT_EXIST:
$driver_message = sprintf( "Constraint '%s' does not exist.", $e->get_data()['name'] );
return $this->new_driver_exception(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1507,7 +1507,7 @@ private function record_drop_check_constraint(
private function extract_column_data( string $table_name, string $column_name, WP_Parser_Node $node, int $position ): array {
list ( $data_type, $column_type ) = $this->get_column_data_types( $node );

$default = $this->get_column_default( $node, $data_type );
$default = $this->get_column_default( $node, $data_type, $column_name );
$nullable = $this->get_column_nullable( $node );
$key = $this->get_column_key( $node );
$extra = $this->get_column_extra( $node );
Expand Down Expand Up @@ -2043,11 +2043,12 @@ private function get_table_comment( WP_Parser_Node $node ): string {
/**
* Extract column default value from the "columnDefinition" or "fieldDefinition" AST node.
*
* @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
* @param string $data_type The column data type as stored in information schema.
* @return string|null The column default as stored in information schema.
* @param WP_Parser_Node $node The "columnDefinition" or "fieldDefinition" AST node.
* @param string $data_type The column data type as stored in information schema.
* @param string $column_name The column name.
* @return string|null The column default as stored in information schema.
*/
private function get_column_default( WP_Parser_Node $node, string $data_type ): ?string {
private function get_column_default( WP_Parser_Node $node, string $data_type, string $column_name ): ?string {
$default_attr = null;
foreach ( $node->get_descendant_nodes( 'columnAttribute' ) as $attr ) {
if ( $attr->has_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) {
Expand Down Expand Up @@ -2081,7 +2082,7 @@ private function get_column_default( WP_Parser_Node $node, string $data_type ):
// A signed number, such as "-5", has no "literal" child node.
return $this->get_value( $signed_literal );
}
return $this->get_literal_default( $literal, $data_type );
return $this->get_literal_default( $literal, $data_type, $column_name );
}

// DEFAULT (expression) - MySQL 8.0.13+ supports exprWithParentheses
Expand All @@ -2096,11 +2097,12 @@ private function get_column_default( WP_Parser_Node $node, string $data_type ):
/**
* Extract and normalize a literal default value.
*
* @param WP_Parser_Node $literal The "literal" AST node.
* @param string $data_type The column data type as stored in information schema.
* @return string|null The default value as stored in information schema.
* @param WP_Parser_Node $literal The "literal" AST node.
* @param string $data_type The column data type as stored in information schema.
* @param string $column_name The column name.
* @return string|null The default value as stored in information schema.
*/
private function get_literal_default( WP_Parser_Node $literal, string $data_type ): ?string {
private function get_literal_default( WP_Parser_Node $literal, string $data_type, string $column_name ): ?string {
// DEFAULT NULL
if ( $literal->has_child_node( 'nullLiteral' ) ) {
return null;
Expand All @@ -2116,7 +2118,15 @@ private function get_literal_default( WP_Parser_Node $literal, string $data_type
$default = $this->get_value( $literal );

if ( 'bit' === $data_type ) {
return $this->get_bit_default( $default ) ?? $default;
/*
* @TODO: Validate and normalize defaults from their AST and the full
* column definition before storing them in the information schema.
*/
$bit_default = $this->get_bit_default( $default );
if ( null === $bit_default ) {
throw WP_SQLite_Information_Schema_Exception::invalid_default_value( $column_name );
}
return $bit_default;
}

/*
Expand All @@ -2142,15 +2152,26 @@ private function get_literal_default( WP_Parser_Node $literal, string $data_type
private function get_bit_default( string $default_value ): ?string {
$value = strtolower( $default_value );

// An empty string coerces to zero.
if ( '' === $value ) {
return "b'0'";
}

// Bit literal, e.g. b'101' or 0b101.
if ( str_starts_with( $value, "b'" ) || str_starts_with( $value, '0b' ) ) {
$bits = ltrim( rtrim( substr( $value, 2 ), "'" ), '0' );
if (
preg_match( "/\Ab'([01]*)'\z/", $value, $matches )
|| preg_match( '/\A0b([01]+)\z/', $value, $matches )
) {
$bits = ltrim( $matches[1], '0' );
return "b'" . ( '' === $bits ? '0' : $bits ) . "'";
}

// Hex literal, e.g. x'05' or 0x05.
if ( str_starts_with( $value, "x'" ) || str_starts_with( $value, '0x' ) ) {
return "b'" . decbin( hexdec( rtrim( substr( $value, 2 ), "'" ) ) ) . "'";
if (
preg_match( "/\Ax'([0-9a-f]*)'\z/", $value, $matches )
|| preg_match( '/\A0x([0-9a-f]+)\z/', $value, $matches )
) {
return "b'" . decbin( hexdec( $matches[1] ) ) . "'";
}

// Decimal, e.g. 5, or a numeric string literal such as '0'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class WP_SQLite_Information_Schema_Exception extends Exception {
const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found';
const TYPE_CONSTRAINT_DOES_NOT_EXIST = 'constraint-does-not-exist';
const TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME = 'multiple-constraints-with-name';
const TYPE_INVALID_DEFAULT_VALUE = 'invalid-default-value';

/**
* The exception type.
Expand Down Expand Up @@ -151,4 +152,18 @@ public static function multiple_constraints_with_name( string $name ): WP_SQLite
array( 'name' => $name )
);
}

/**
* Create an invalid default value exception.
*
* @param string $column_name The name of the affected column.
* @return self The exception instance.
*/
public static function invalid_default_value( string $column_name ): WP_SQLite_Information_Schema_Exception {
return new self(
self::TYPE_INVALID_DEFAULT_VALUE,
sprintf( "Invalid default value for '%s'.", $column_name ),
array( 'column_name' => $column_name )
);
}
}
61 changes: 61 additions & 0 deletions packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ public function testShowCreateTableWithBitDefaultValues(): void {
$this->assertQuery(
"CREATE TABLE _tmp_bit_defaults (
id INT DEFAULT 0,
empty_string BIT(1) DEFAULT '',
quoted_zero BIT(1) DEFAULT '0',
integer_five BIT(4) DEFAULT 5,
bit_literal_five BIT(4) DEFAULT b'0101',
Expand All @@ -718,6 +719,7 @@ public function testShowCreateTableWithBitDefaultValues(): void {
$this->assertQuery( 'SHOW CREATE TABLE _tmp_bit_defaults;' );
$results = $this->last_result;
$create_table = $results[0]->{'Create Table'};
$this->assertStringContainsString( "`empty_string` bit(1) DEFAULT b'0'", $create_table );
$this->assertStringContainsString( "`quoted_zero` bit(1) DEFAULT b'0'", $create_table );
$this->assertStringContainsString( "`integer_five` bit(4) DEFAULT b'101'", $create_table );
$this->assertStringContainsString( "`bit_literal_five` bit(4) DEFAULT b'101'", $create_table );
Expand All @@ -734,6 +736,7 @@ public function testShowCreateTableWithBitDefaultValues(): void {
array(
(object) array(
'id' => '1',
'empty_string' => '0',
'quoted_zero' => '0',
'integer_five' => '5',
'bit_literal_five' => '5',
Expand All @@ -748,6 +751,64 @@ public function testShowCreateTableWithBitDefaultValues(): void {
);
}

/**
* @dataProvider invalidBitDefaultQueries
*/
public function testRejectsInvalidBitDefaultValues( string $query, string $column_name ): void {
$this->assertQuery( 'CREATE TABLE _tmp_invalid_bit_default (id INT, injected BIT(1))' );

$exception = null;
try {
$this->query( $query );
} catch ( WP_MySQL_On_SQLite_Exception $e ) {
$exception = $e;
}

$driver_message = sprintf( "Invalid default value for '%s'", $column_name );
$this->assertInstanceOf( WP_MySQL_On_SQLite_Exception::class, $exception );
$this->assertSame(
'SQLSTATE[42000]: Syntax error or access violation: 1067 ' . $driver_message,
$exception->getMessage()
);
$this->assertSame( '42000', $exception->getCode() );
$this->assertSame( array( '42000', 1067, $driver_message ), $exception->errorInfo );
}

public static function invalidBitDefaultQueries(): array {
$payload = "0,\n PRIMARY KEY (`id`)\n); DROP TABLE users; --";

return array(
'CREATE TABLE' => array(
"CREATE TABLE _tmp_created_invalid_bit_default (injected BIT(1) DEFAULT '$payload')",
'injected',
),
'ALTER TABLE ADD' => array(
"ALTER TABLE _tmp_invalid_bit_default ADD COLUMN injected_2 BIT(1) DEFAULT '$payload'",
'injected_2',
),
'ALTER TABLE MODIFY' => array(
"ALTER TABLE _tmp_invalid_bit_default MODIFY COLUMN injected BIT(1) DEFAULT '$payload'",
'injected',
),
'bit-like string' => array(
"ALTER TABLE _tmp_invalid_bit_default MODIFY COLUMN injected BIT(1) DEFAULT 'b''0''; DROP TABLE users; -- '",
'injected',
),
'0b-like string' => array(
"ALTER TABLE _tmp_invalid_bit_default MODIFY COLUMN injected BIT(1) DEFAULT '0b0''; DROP TABLE users; -- '",
'injected',
),
'hex-like string' => array(
"ALTER TABLE _tmp_invalid_bit_default MODIFY COLUMN injected BIT(1) DEFAULT 'x''05''; DROP TABLE users; -- '",
'injected',
),
'0x-like string' => array(
"ALTER TABLE _tmp_invalid_bit_default MODIFY COLUMN injected BIT(1) DEFAULT '0x05; DROP TABLE users; -- '",
'injected',
),
);
}

public function testUpdateWithBitColumnDefaultValue(): void {
$this->assertQuery(
"CREATE TABLE _tmp_bit_defaults (
Expand Down
Loading