Skip to content
13 changes: 13 additions & 0 deletions inc/native/class-wp-markdown-native-query-executor.php
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,9 @@ function ( array $left, array $right ) use ( $plan, $sources ): int {
private function allows_residual_scan( array $predicates, WP_Markdown_Native_Table_Schema $schema ): bool {
$indexed = $this->indexed_columns( $schema );
foreach ( $predicates as $predicate ) {
if ( in_array( $predicate->operator(), array( 'OR', 'LOWER =' ), true ) ) {
continue;
}
if ( $this->predicate_uses_like( $predicate ) ) {
continue;
}
Expand Down Expand Up @@ -627,6 +630,11 @@ private function supports_predicate( WP_Markdown_Native_Table_Schema $schema, WP
if ( in_array( $predicate->operator(), array( 'IS NULL', 'IS NOT NULL' ), true ) ) {
return $schema->has_column( $predicate->column() );
}
if ( 'LOWER =' === $predicate->operator() ) {
return $schema->has_column( $predicate->column() )
&& 1 === count( $predicate->values() )
&& null !== WP_Markdown_Native_Runtime_Factory::normalize_ascii_ci( $predicate->values()[0] );
}
return $schema->allows_lookup( $predicate->column(), $predicate->operator(), $predicate->values() )
|| $schema->allows_filter( $predicate->column(), $predicate->operator(), $predicate->values() );
}
Expand Down Expand Up @@ -677,6 +685,11 @@ private function matches_predicate( array $row, WP_Markdown_Native_Query_Predica
if ( 'IS NOT NULL' === $predicate->operator() ) {
return null !== ( $row[ $predicate->column() ] ?? null );
}
if ( 'LOWER =' === $predicate->operator() ) {
$left = WP_Markdown_Native_Runtime_Factory::normalize_ascii_ci( $row[ $predicate->column() ] ?? null );
$right = WP_Markdown_Native_Runtime_Factory::normalize_ascii_ci( $predicate->values()[0] ?? null );
return null !== $left && null !== $right && $left === $right;
}
$negated = in_array( $predicate->operator(), array( 'NOT IN', 'NOT LIKE' ), true );
if ( $negated && null === ( $row[ $predicate->column() ] ?? null ) ) {
return false;
Expand Down
52 changes: 37 additions & 15 deletions inc/native/class-wp-markdown-native-query-parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,9 @@ private function is_on(): bool {
* Parse a WHERE expression.
*
* AND binds tighter than OR, matching SQL. A disjunction is accepted only
* when every alternative is equality on the same column, which is the
* membership shape WordPress uses for `post_status` lists. Cross-column
* OR and inequality OR stay fail-closed.
* when every alternative is a supported equality predicate. Same-column
* alternatives collapse to membership; cross-column alternatives retain
* an explicit bounded disjunction. Inequality OR stays fail-closed.
*
* @return array<int,WP_Markdown_Native_SQL_Predicate>
*/
Expand Down Expand Up @@ -439,24 +439,25 @@ private function coalesce_disjunction( array $groups, int $sql_offset ): array {
if ( count( $likes ) === count( $groups ) ) {
return array( new WP_Markdown_Native_SQL_Predicate( $likes[0]->column(), 'OR', array(), $likes ) );
}
$column = null;
$qualifier = null;
$values = array();
$identifier = null;
$column = null;
$qualifier = null;
$values = array();
$identifier = null;
$same_column = true;
foreach ( $groups as $group ) {
if ( 1 !== count( $group ) ) {
throw new WP_Markdown_Native_SQL_Parse_Error(
'unsupported_or',
$sql_offset,
'mdi-native supports OR only as same-column equality or LIKE alternatives.'
'mdi-native supports OR only as equality or LIKE alternatives.'
);
}
$predicate = $group[0];
if ( ! in_array( $predicate->operator(), array( '=', 'IN', 'IS NULL' ), true ) ) {
if ( ! in_array( $predicate->operator(), array( '=', 'IN', 'IS NULL', 'LOWER =' ), true ) ) {
throw new WP_Markdown_Native_SQL_Parse_Error(
'unsupported_or',
$sql_offset,
'mdi-native supports OR only as same-column equality or LIKE alternatives.'
'mdi-native supports OR only as equality or LIKE alternatives.'
);
}
$name = $predicate->column()->name();
Expand All @@ -466,15 +467,14 @@ private function coalesce_disjunction( array $groups, int $sql_offset ): array {
$qualifier = $qual;
$identifier = $predicate->column();
} elseif ( $column !== $name || $qualifier !== $qual ) {
throw new WP_Markdown_Native_SQL_Parse_Error(
'unsupported_or',
$sql_offset,
'mdi-native supports OR only as same-column equality or LIKE alternatives.'
);
$same_column = false;
}
$values = array_merge( $values, $predicate->values() );
}
$alternatives = array_map( static fn( array $group ): WP_Markdown_Native_SQL_Predicate => $group[0], $groups );
if ( ! $same_column ) {
return array( new WP_Markdown_Native_SQL_Predicate( $identifier, 'OR', array(), $alternatives ) );
}
foreach ( $alternatives as $alternative ) {
if ( 'IS NULL' === $alternative->operator() ) {
return array( new WP_Markdown_Native_SQL_Predicate( $identifier, 'OR', array(), $alternatives ) );
Expand Down Expand Up @@ -547,6 +547,9 @@ private function matches_function( string $function ): bool {
}

private function predicate(): WP_Markdown_Native_SQL_Predicate {
if ( $this->matches_function( 'LOWER' ) ) {
return $this->lower_equality_predicate();
}
$column = $this->identifier();
if ( $this->match_keyword( 'IS' ) ) {
$operator = $this->match_keyword( 'NOT' ) ? 'IS NOT NULL' : 'IS NULL';
Expand All @@ -573,6 +576,25 @@ private function predicate(): WP_Markdown_Native_SQL_Predicate {
return new WP_Markdown_Native_SQL_Predicate( $column, 'IN', $this->in_list() );
}

private function lower_equality_predicate(): WP_Markdown_Native_SQL_Predicate {
$this->unqualified_identifier();
$this->expect_type( WP_Markdown_Native_SQL_Token::LEFT_PAREN );
$column = $this->identifier();
$this->expect_type( WP_Markdown_Native_SQL_Token::RIGHT_PAREN );
$this->expect_type( WP_Markdown_Native_SQL_Token::EQUALS );
if ( ! $this->matches_function( 'LOWER' ) ) {
$this->unsupported( $this->current() );
}
$this->unqualified_identifier();
$this->expect_type( WP_Markdown_Native_SQL_Token::LEFT_PAREN );
$value = $this->literal();
$this->expect_type( WP_Markdown_Native_SQL_Token::RIGHT_PAREN );
if ( ! is_string( $value->value() ) || 1 === preg_match( '/[^\x00-\x7F]/', $value->value() ) ) {
throw new WP_Markdown_Native_SQL_Parse_Error( 'unsupported_literal', $value->sql_offset(), 'mdi-native LOWER equality requires an ASCII string literal.' );
}
return new WP_Markdown_Native_SQL_Predicate( $column, 'LOWER =', array( $value ) );
}

private function like_predicate( WP_Markdown_Native_SQL_Identifier $column, string $operator ): WP_Markdown_Native_SQL_Predicate {
$pattern = $this->literal();
if ( ! is_string( $pattern->value() ) ) {
Expand Down
25 changes: 14 additions & 11 deletions inc/native/class-wp-markdown-native-schema-catalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,21 @@ public static function indexed_snapshot_schema(
}
}
$overlay = array( 'columns' => array(), 'natural_order' => $identity, 'order_columns' => $order_columns );
$ascii = static fn( array $values ): bool => array() === array_filter(
$values,
static fn( mixed $value ): bool => ! is_string( $value ) || 1 === preg_match( '/[^\x00-\x7F]/', $value )
);
foreach ( $definition['columns'] as $name => $column ) {
$overlay['columns'][ $name ] = array(
'filter_operators' => self::is_integer( $column['type'] ) || self::is_decimal( $column['type'] )
? array( '=', 'IN', 'NOT IN', '<>' )
: ( in_array( $column['type'], array( 'char', 'varchar', 'enum', 'set', 'tinytext', 'text', 'mediumtext', 'longtext' ), true ) ? array( 'LIKE', 'NOT LIKE' ) : array() ),
);
if ( self::is_integer( $column['type'] ) || self::is_decimal( $column['type'] ) ) {
$overlay['columns'][ $name ] = array( 'filter_operators' => array( '=', 'IN', 'NOT IN', '<>' ) );
} elseif ( in_array( $column['type'], array( 'char', 'varchar', 'enum', 'set', 'tinytext', 'text', 'mediumtext', 'longtext' ), true ) ) {
$overlay['columns'][ $name ] = array(
'filter_operators' => array( '=', 'IN', 'NOT IN', '<>', 'LIKE', 'NOT LIKE' ),
'filter_validator' => $ascii,
);
} else {
$overlay['columns'][ $name ] = array( 'filter_operators' => array() );
}
}
foreach ( $definition['indexes'] as $index ) {
$unique = true === ( $index['unique'] ?? false );
Expand All @@ -332,12 +341,6 @@ public static function indexed_snapshot_schema(
continue;
}
if ( in_array( $type, array( 'char', 'varchar', 'enum', 'set' ), true ) ) {
$ascii = static fn( array $values ): bool => array() === array_filter(
$values,
static fn( mixed $value ): bool => ! is_string( $value ) || 1 === preg_match( '/[^\x00-\x7F]/', $value )
);
$overlay['columns'][ $name ]['filter_operators'] = array( '=', 'IN', 'NOT IN', '<>', 'LIKE', 'NOT LIKE' );
$overlay['columns'][ $name ]['filter_validator'] = $ascii;
if ( $unique ) {
$overlay['columns'][ $name ]['lookup_operators'] = array( '=', 'IN' );
$overlay['columns'][ $name ]['lookup_validator'] = $ascii;
Expand Down
64 changes: 61 additions & 3 deletions rigs/mdi-native/rig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,54 @@
"bench_env": {
"BENCH_CORPUS_SIZE": "100"
},
"wordpress_runtime_workloads": [
{
"id": "concurrent-read",
"source": "rig",
"overridesDiscovered": true,
"run": [
{
"type": "external-http-load",
"url": "/wp-json/mdi-bench/v1/concurrent-read",
"requestCount": 20,
"concurrency": 4,
"expectedStatuses": [
200
],
"metric-prefix": "concurrent_read"
}
]
}
],
"wp_config_defines": {
"MARKDOWN_DB_BACKEND": "mdi-native"
}
"MARKDOWN_DB_BACKEND": "mdi-native",
"MARKDOWN_DB_CONTENT_DIR": "/wordpress/wp-content/db",
"MARKDOWN_DB_STATE_DIR": "/wordpress/wp-content/db",
"MDI_BENCH_EXPECTED_WPDB_CLASS": "WP_Markdown_Native_WPDB"
},
"wp_codebox_bench_mounts": [
{
"source": "${components.markdown-database-integration.path}/db.php",
"target": "/wordpress/wp-content/db.php",
"type": "file",
"mode": "readonly",
"phase": "pre-install"
},
{
"source": "${components.markdown-database-integration.path}/tests/fixtures/native-bench-state",
"target": "/wordpress/wp-content/db",
"type": "directory",
"mode": "readonly",
"phase": "pre-install"
},
{
"source": "${components.markdown-database-integration.path}/tests/bench-fixtures/concurrent-read-endpoint.php",
"target": "/wordpress/wp-content/mu-plugins/mdi-bench-concurrent-read.php",
"type": "file",
"mode": "readonly",
"phase": "pre-install"
}
]
}
}
}
Expand Down Expand Up @@ -52,6 +97,15 @@
},
{
"path": "${package.root}/tests/bench/read-heavy.php"
},
{
"path": "${package.root}/tests/bench/wiki-hierarchy.php"
},
{
"path": "${package.root}/tests/bench/plugin-table-inventory.php"
},
{
"path": "${package.root}/tests/bench/transaction-heavy.php"
}
]
},
Expand All @@ -60,7 +114,10 @@
"boot-timing",
"bulk-import",
"obsidian-bursty",
"read-heavy"
"read-heavy",
"wiki-hierarchy",
"plugin-table-inventory",
"transaction-heavy"
]
},
"pipeline": {
Expand All @@ -70,6 +127,7 @@
"label": "MDI plugin checkout exists",
"file": "${components.markdown-database-integration.path}/markdown-database-integration.php",
"component": "markdown-database-integration",
"component_path_contains": "markdown-database-integration",
"remediation": "Set HOMEBOY_RIG_COMPONENT_PATH__MDI_NATIVE__MARKDOWN_DATABASE_INTEGRATION to an MDI checkout."
}
],
Expand Down
48 changes: 46 additions & 2 deletions rigs/mdi-sqlite/rig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,38 @@
"wordpress": {
"bench_env": {
"BENCH_CORPUS_SIZE": "100"
}
},
"wordpress_runtime_workloads": [
{
"id": "concurrent-read",
"source": "rig",
"overridesDiscovered": true,
"run": [
{
"type": "external-http-load",
"url": "/wp-json/mdi-bench/v1/concurrent-read",
"requestCount": 20,
"concurrency": 4,
"expectedStatuses": [
200
],
"metric-prefix": "concurrent_read"
}
]
}
],
"wp_config_defines": {
"MDI_BENCH_EXPECTED_WPDB_CLASS": "WP_SQLite_DB"
},
"wp_codebox_bench_mounts": [
{
"source": "${components.markdown-database-integration.path}/tests/bench-fixtures/concurrent-read-endpoint.php",
"target": "/wordpress/wp-content/mu-plugins/mdi-bench-concurrent-read.php",
"type": "file",
"mode": "readonly",
"phase": "pre-install"
}
]
}
}
}
Expand Down Expand Up @@ -49,6 +80,15 @@
},
{
"path": "${package.root}/tests/bench/read-heavy.php"
},
{
"path": "${package.root}/tests/bench/wiki-hierarchy.php"
},
{
"path": "${package.root}/tests/bench/plugin-table-inventory.php"
},
{
"path": "${package.root}/tests/bench/transaction-heavy.php"
}
]
},
Expand All @@ -57,7 +97,10 @@
"boot-timing",
"bulk-import",
"obsidian-bursty",
"read-heavy"
"read-heavy",
"wiki-hierarchy",
"plugin-table-inventory",
"transaction-heavy"
]
},
"pipeline": {
Expand All @@ -67,6 +110,7 @@
"label": "MDI plugin checkout exists",
"file": "${components.markdown-database-integration.path}/markdown-database-integration.php",
"component": "markdown-database-integration",
"component_path_contains": "markdown-database-integration",
"remediation": "Set HOMEBOY_RIG_COMPONENT_PATH__MDI_SQLITE__MARKDOWN_DATABASE_INTEGRATION to an MDI checkout."
}
],
Expand Down
Loading