From a5a2383e2ee0346a9a07b58e8586b38425090b87 Mon Sep 17 00:00:00 2001 From: bsrikanth-mariadb Date: Wed, 5 Aug 2026 17:45:38 +0530 Subject: [PATCH] MDEV-40553: unprintable gis ranges in trace and context When ranges were specified in a query for GIS types, the recorded trace and context couldn't print the range information. Instead, it only showed unprintable_geometry_value. This PR extends the geometric field type Field_geom to print appropriate key value, along with the comparison operator when printed in the range. Key value is decoded from binary if applicable, and is recorded in WKT format. For spatial indexes, the operators like MDRWITHIN, MDRCONTAINS, etc... are stored appropriately, and for normal indexes, operators like <, <=, >, >=, etc... are recorded appropriately. Implementation Details: - 1. overwrite print_key_value() for Field_geom to print the decoded binary value in WKT format. If binary value can't be decoded appropriately to WKT format, then the binary value itself is recorded. 2. Add an argument imagetype to Field::print_key_part_value(), to determine if an index key part value is to be printed in WKT or binary format. For Geometric type, imagetype is set to itMBR. When printing ranges, the MBR operators are printed as well, as implemented in print_mbr_range_operator(). --- .../main/opt_context_store_stats.result | 210 ++++++++++++ mysql-test/main/opt_context_store_stats.test | 96 ++++++ mysql-test/main/opt_trace.result | 299 ++++++++++++++++++ mysql-test/main/opt_trace.test | 98 ++++++ sql/field.cc | 5 +- sql/field.h | 3 +- sql/opt_context_store_replay.cc | 16 +- sql/opt_range.cc | 67 +++- sql/opt_range.h | 3 +- sql/sql_type_geom.cc | 103 ++++++ sql/sql_type_geom.h | 7 +- 11 files changed, 884 insertions(+), 23 deletions(-) diff --git a/mysql-test/main/opt_context_store_stats.result b/mysql-test/main/opt_context_store_stats.result index 99cc4b02a98a6..b1c3dd717e306 100644 --- a/mysql-test/main/opt_context_store_stats.result +++ b/mysql-test/main/opt_context_store_stats.result @@ -646,4 +646,214 @@ index_name ranges num_rows max_index_blocks max_row_blocks a ["(10) <= (a) <= (10)"] 49 1 11 # == End of optimizer context drop table t1; +# +# MDEV-40533: unprintable gis ranges in trace and context +# +create table t1 ( +id int, +g1 geometry not null, +g2 geometry, +key k_prefix (g1(5)), +key k_full (g1(200)), +key k_null (g2(200)) +) engine=myisam; +insert into t1 select seq, point(seq, seq), point(seq, seq) from seq_1_to_5; +insert into t1 values (6, point(6,6), NULL); +analyze table t1; +Table Op Msg_type Msg_text +test.t1 analyze status OK +# The index holds the whole value: it is printed in the WKT format +explain +select id from t1 force index (k_full) where g1=point(2,2); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ref k_full k_full 202 const 1 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t1 6 NULL NULL +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +k_full ["(POINT(2 2)) <= (g1) <= (POINT(2 2))"] 1 1 1 +# == End of optimizer context +# multiple ranges +explain +select id from t1 force index (k_full) +where g1=point(3,3) or g1=linestring(point(1,1), point(2,2)); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 range k_full k_full 202 NULL 2 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t1 6 NULL NULL +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +k_full [ + "(POINT(3 3)) <= (g1) <= (POINT(3 3))", + "(LINESTRING(1 1,2 2)) <= (g1) <= (LINESTRING(1 1,2 2))" + ] 2 2 1 +# == End of optimizer context +# NULL values and multiple ranges +explain +select id from t1 force index (k_null) +where g2=point(3,3) or g2 is null or g2=linestring(point(1,1), point(2,2)); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 range k_null k_null 203 NULL 3 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t1 6 NULL NULL +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +k_null [ + "(NULL) <= (g2) <= (NULL)", + "(POINT(3 3)) <= (g2) <= (POINT(3 3))", + "(LINESTRING(1 1,2 2)) <= (g2) <= (LINESTRING(1 1,2 2))" + ] 3 3 1 +# == End of optimizer context +# The index holds only a prefix of the value, so it cannot be converted +# to WKT. It is printed as a binary string, like any other BLOB prefix. +explain +select id from t1 force index (k_prefix) where g1=point(2,2); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ref k_prefix k_prefix 7 const 6 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t1 6 NULL NULL +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +k_prefix ["(\\\\x00\\\\x00\\\\x00\\\\x00\\\\x01) <= (g1) <= (\\\\x00\\\\x00\\\\x00\\\\x00\\\\x01)"] 6 1 1 +# == End of optimizer context +drop table t1; +# +# A SPATIAL index stores the MBR of the value. It is printed as the +# WKT of the MBR together with the spatial relation the range uses. +# +create table t2 ( +id int, +g geometry not null, +spatial key (g) +) engine=myisam; +insert into t2 select seq, point(seq, seq) from seq_1_to_20; +insert into t2 values +(21, geomfromtext('LINESTRING(0 0, 5 8)')), +(22, geomfromtext('POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))')); +analyze table t2; +Table Op Msg_type Msg_text +test.t2 analyze status OK +# mbrcontains(value, column) +explain +select id from t2 where mbrcontains(geomfromtext('POLYGON((0 0,0 3,3 3,3 0,0 0))'), g); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRWITHIN (POLYGON((0 0,3 0,3 3,0 3,0 0)))"] 4 1 1 +# == End of optimizer context +# mbrwithin(column, value) +explain +select id from t2 where mbrwithin(g, geomfromtext('LINESTRING(1 1, 9 12)')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 9 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRWITHIN (POLYGON((1 1,9 1,9 12,1 12,1 1)))"] 9 1 1 +# == End of optimizer context +# The other spatial relations +explain +select id from t2 where mbrcontains(g, geomfromtext('POINT(2 2)')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 3 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRCONTAINS (POLYGON((2 2,2 2,2 2,2 2,2 2)))"] 3 1 1 +# == End of optimizer context +explain +select id from t2 where mbrequals(g, geomfromtext('POINT(7 7)')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 1 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBREQUALS (POLYGON((7 7,7 7,7 7,7 7,7 7)))"] 1 1 1 +# == End of optimizer context +explain +select id from t2 where mbrintersects(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRINTERSECTS (POLYGON((0 0,2 0,2 2,0 2,0 0)))"] 4 1 1 +# == End of optimizer context +# mbrtouches/mbroverlaps all use the mbrintersects relation +explain +select id from t2 where mbrtouches(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRINTERSECTS (POLYGON((0 0,2 0,2 2,0 2,0 0)))"] 4 1 1 +# == End of optimizer context +explain +select id from t2 where mbroverlaps(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRINTERSECTS (POLYGON((0 0,2 0,2 2,0 2,0 0)))"] 4 1 1 +# == End of optimizer context +explain +select id from t2 where mbrdisjoint(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 ALL g NULL NULL NULL 22 Using where +# == Optimizer Context +# === Tables +# Tables in the context +table_name file_stat_records index_name rec_per_key +test.t2 22 g ["22"] +# === Range accesses +index_name ranges num_rows max_index_blocks max_row_blocks +g ["(g) MBRDISJOINT (POLYGON((0 0,2 0,2 2,0 2,0 0)))"] 2147483647 0 0 +Warnings: +Warning 1264 Out of range value for column 'num_rows' at row 1 +# == End of optimizer context +drop table t2; # End of 13.1 tests diff --git a/mysql-test/main/opt_context_store_stats.test b/mysql-test/main/opt_context_store_stats.test index cce698d51692a..981eed702ab36 100644 --- a/mysql-test/main/opt_context_store_stats.test +++ b/mysql-test/main/opt_context_store_stats.test @@ -470,4 +470,100 @@ select * from t1 partition (p1) where a=10; drop table t1; +--echo # +--echo # MDEV-40533: unprintable gis ranges in trace and context +--echo # +create table t1 ( + id int, + g1 geometry not null, + g2 geometry, + key k_prefix (g1(5)), + key k_full (g1(200)), + key k_null (g2(200)) +) engine=myisam; + +insert into t1 select seq, point(seq, seq), point(seq, seq) from seq_1_to_5; +insert into t1 values (6, point(6,6), NULL); +analyze table t1; + +--echo # The index holds the whole value: it is printed in the WKT format +explain +select id from t1 force index (k_full) where g1=point(2,2); +--source include/opt_context_list_tables_and_ranges.inc + +--echo # multiple ranges +explain +select id from t1 force index (k_full) +where g1=point(3,3) or g1=linestring(point(1,1), point(2,2)); +--source include/opt_context_list_tables_and_ranges.inc + +--echo # NULL values and multiple ranges +explain +select id from t1 force index (k_null) +where g2=point(3,3) or g2 is null or g2=linestring(point(1,1), point(2,2)); +--source include/opt_context_list_tables_and_ranges.inc + +--echo # The index holds only a prefix of the value, so it cannot be converted +--echo # to WKT. It is printed as a binary string, like any other BLOB prefix. +explain +select id from t1 force index (k_prefix) where g1=point(2,2); +--source include/opt_context_list_tables_and_ranges.inc + +drop table t1; + +--echo # +--echo # A SPATIAL index stores the MBR of the value. It is printed as the +--echo # WKT of the MBR together with the spatial relation the range uses. +--echo # + +create table t2 ( + id int, + g geometry not null, + spatial key (g) +) engine=myisam; + +insert into t2 select seq, point(seq, seq) from seq_1_to_20; +insert into t2 values + (21, geomfromtext('LINESTRING(0 0, 5 8)')), + (22, geomfromtext('POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))')); +analyze table t2; + +--echo # mbrcontains(value, column) +explain +select id from t2 where mbrcontains(geomfromtext('POLYGON((0 0,0 3,3 3,3 0,0 0))'), g); +--source include/opt_context_list_tables_and_ranges.inc + +--echo # mbrwithin(column, value) +explain +select id from t2 where mbrwithin(g, geomfromtext('LINESTRING(1 1, 9 12)')); +--source include/opt_context_list_tables_and_ranges.inc + +--echo # The other spatial relations +explain +select id from t2 where mbrcontains(g, geomfromtext('POINT(2 2)')); +--source include/opt_context_list_tables_and_ranges.inc + +explain +select id from t2 where mbrequals(g, geomfromtext('POINT(7 7)')); +--source include/opt_context_list_tables_and_ranges.inc + +explain +select id from t2 where mbrintersects(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +--source include/opt_context_list_tables_and_ranges.inc + +--echo # mbrtouches/mbroverlaps all use the mbrintersects relation +explain +select id from t2 where mbrtouches(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +--source include/opt_context_list_tables_and_ranges.inc + +explain +select id from t2 where mbroverlaps(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +--source include/opt_context_list_tables_and_ranges.inc + +explain +select id from t2 where mbrdisjoint(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +--source include/opt_context_list_tables_and_ranges.inc + +drop table t2; + --echo # End of 13.1 tests diff --git a/mysql-test/main/opt_trace.result b/mysql-test/main/opt_trace.result index 6de4d363f02c9..f2f6a6135fd35 100644 --- a/mysql-test/main/opt_trace.result +++ b/mysql-test/main/opt_trace.result @@ -13845,6 +13845,305 @@ select json_valid(@trace); json_valid(@trace) 1 drop table t1; +# +# MDEV-40533: unprintable gis ranges in trace and context +# +create table t1 ( +id int, +g1 geometry not null, +g2 geometry, +key k_prefix (g1(5)), +key k_full (g1(200)), +key k_null (g2(200)) +) engine=myisam; +insert into t1 select seq, point(seq, seq), point(seq, seq) from seq_1_to_5; +insert into t1 values (6, point(6,6), NULL); +analyze table t1; +Table Op Msg_type Msg_text +test.t1 analyze status Engine-independent statistics collected +test.t1 analyze status OK +# The index holds the whole value: it is printed in the WKT format +explain +select id from t1 force index (k_full) where g1=point(2,2); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ref k_full k_full 202 const 1 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "k_full", + "ranges": + ["(POINT(2 2)) <= (g1) <= (POINT(2 2))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 1, + "cost": 0.002574553, + "chosen": true + } + ] +] +# multiple ranges +explain +select id from t1 force index (k_full) +where g1=point(3,3) or g1=linestring(point(1,1), point(2,2)); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 range k_full k_full 202 NULL 2 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "k_full", + "ranges": + [ + "(POINT(3 3)) <= (g1) <= (POINT(3 3))", + "(LINESTRING(1 1,2 2)) <= (g1) <= (LINESTRING(1 1,2 2))" + ], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 2, + "cost": 0.004394164, + "chosen": true + } + ] +] +# NULL values and multiple ranges +explain +select id from t1 force index (k_null) +where g2=point(3,3) or g2 is null or g2=linestring(point(1,1), point(2,2)); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 range k_null k_null 203 NULL 3 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "k_null", + "ranges": + [ + "(NULL) <= (g2) <= (NULL)", + "(POINT(3 3)) <= (g2) <= (POINT(3 3))", + "(LINESTRING(1 1,2 2)) <= (g2) <= (LINESTRING(1 1,2 2))" + ], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 3, + "cost": 0.006213775, + "chosen": true + } + ] +] +# The index holds only a prefix of the value, so it cannot be converted +# to WKT. It is printed as a binary string, like any other BLOB prefix. +explain +select id from t1 force index (k_prefix) where g1=point(2,2); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ref k_prefix k_prefix 7 const 6 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "k_prefix", + "ranges": + ["(\\x00\\x00\\x00\\x00\\x01) <= (g1) <= (\\x00\\x00\\x00\\x00\\x01)"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 6, + "cost": 0.008743898, + "chosen": true + } + ] +] +drop table t1; +# +# A SPATIAL index stores the MBR of the value. It is printed as the +# WKT of the MBR together with the spatial relation the range uses. +# +create table t2 ( +id int, +g geometry not null, +spatial key (g) +) engine=myisam; +insert into t2 select seq, point(seq, seq) from seq_1_to_20; +insert into t2 values +(21, geomfromtext('LINESTRING(0 0, 5 8)')), +(22, geomfromtext('POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))')); +analyze table t2; +Table Op Msg_type Msg_text +test.t2 analyze status Engine-independent statistics collected +test.t2 analyze status OK +# mbrcontains(value, column) +explain +select id from t2 where mbrcontains(geomfromtext('POLYGON((0 0,0 3,3 3,3 0,0 0))'), g); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBRWITHIN (POLYGON((0 0,3 0,3 3,0 3,0 0)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 4, + "cost": 0.00627616, + "chosen": true + } + ] +] +# mbrwithin(column, value) +explain +select id from t2 where mbrwithin(g, geomfromtext('LINESTRING(1 1, 9 12)')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 9 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBRWITHIN (POLYGON((1 1,9 1,9 12,1 12,1 1)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 9, + "cost": 0.012445505, + "chosen": true + } + ] +] +# The other spatial relations +explain +select id from t2 where mbrcontains(g, geomfromtext('POINT(2 2)')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 3 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBRCONTAINS (POLYGON((2 2,2 2,2 2,2 2,2 2)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 3, + "cost": 0.005042291, + "chosen": true + } + ] +] +explain +select id from t2 where mbrequals(g, geomfromtext('POINT(7 7)')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 1 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBREQUALS (POLYGON((7 7,7 7,7 7,7 7,7 7)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 1, + "cost": 0.002574553, + "chosen": true + } + ] +] +explain +select id from t2 where mbrintersects(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBRINTERSECTS (POLYGON((0 0,2 0,2 2,0 2,0 0)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 4, + "cost": 0.00627616, + "chosen": true + } + ] +] +# mbrtouches/mbroverlaps all use the mbrintersects relation +explain +select id from t2 where mbrtouches(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBRINTERSECTS (POLYGON((0 0,2 0,2 2,0 2,0 0)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 4, + "cost": 0.00627616, + "chosen": true + } + ] +] +explain +select id from t2 where mbroverlaps(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 range g g 34 NULL 4 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g", + "ranges": + ["(g) MBRINTERSECTS (POLYGON((0 0,2 0,2 2,0 2,0 0)))"], + "rowid_ordered": false, + "using_mrr": false, + "index_only": false, + "rows": 4, + "cost": 0.00627616, + "chosen": true + } + ] +] +explain +select id from t2 where mbrdisjoint(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 ALL g NULL NULL NULL 22 Using where +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; +json_detailed(json_extract(trace, '$**.range_scan_alternatives')) +[ + [ + { + "index": "g" + } + ] +] +drop table t2; +# End of 13.1 tests set @@optimizer_switch= @save_optimizer_switch; set @@use_stat_tables= @save_use_stat_tables; set @@histogram_size= @save_histogram_size; diff --git a/mysql-test/main/opt_trace.test b/mysql-test/main/opt_trace.test index ce811547f3f1d..08a8ab835c209 100644 --- a/mysql-test/main/opt_trace.test +++ b/mysql-test/main/opt_trace.test @@ -1364,6 +1364,104 @@ select json_valid(@trace); drop table t1; +--echo # +--echo # MDEV-40533: unprintable gis ranges in trace and context +--echo # +create table t1 ( + id int, + g1 geometry not null, + g2 geometry, + key k_prefix (g1(5)), + key k_full (g1(200)), + key k_null (g2(200)) +) engine=myisam; + +insert into t1 select seq, point(seq, seq), point(seq, seq) from seq_1_to_5; +insert into t1 values (6, point(6,6), NULL); +analyze table t1; + +--echo # The index holds the whole value: it is printed in the WKT format +explain +select id from t1 force index (k_full) where g1=point(2,2); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +--echo # multiple ranges +explain +select id from t1 force index (k_full) +where g1=point(3,3) or g1=linestring(point(1,1), point(2,2)); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +--echo # NULL values and multiple ranges +explain +select id from t1 force index (k_null) +where g2=point(3,3) or g2 is null or g2=linestring(point(1,1), point(2,2)); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +--echo # The index holds only a prefix of the value, so it cannot be converted +--echo # to WKT. It is printed as a binary string, like any other BLOB prefix. +explain +select id from t1 force index (k_prefix) where g1=point(2,2); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +drop table t1; + +--echo # +--echo # A SPATIAL index stores the MBR of the value. It is printed as the +--echo # WKT of the MBR together with the spatial relation the range uses. +--echo # + +create table t2 ( + id int, + g geometry not null, + spatial key (g) +) engine=myisam; + +insert into t2 select seq, point(seq, seq) from seq_1_to_20; +insert into t2 values + (21, geomfromtext('LINESTRING(0 0, 5 8)')), + (22, geomfromtext('POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))')); +analyze table t2; + +--echo # mbrcontains(value, column) +explain +select id from t2 where mbrcontains(geomfromtext('POLYGON((0 0,0 3,3 3,3 0,0 0))'), g); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +--echo # mbrwithin(column, value) +explain +select id from t2 where mbrwithin(g, geomfromtext('LINESTRING(1 1, 9 12)')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +--echo # The other spatial relations +explain +select id from t2 where mbrcontains(g, geomfromtext('POINT(2 2)')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +explain +select id from t2 where mbrequals(g, geomfromtext('POINT(7 7)')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +explain +select id from t2 where mbrintersects(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +--echo # mbrtouches/mbroverlaps all use the mbrintersects relation +explain +select id from t2 where mbrtouches(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +explain +select id from t2 where mbroverlaps(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +explain +select id from t2 where mbrdisjoint(g, geomfromtext('POLYGON((0 0,0 2,2 2,2 0,0 0))')); +select json_detailed(json_extract(trace, '$**.range_scan_alternatives')) from information_schema.optimizer_trace; + +drop table t2; + +--echo # End of 13.1 tests + set @@optimizer_switch= @save_optimizer_switch; set @@use_stat_tables= @save_use_stat_tables; set @@histogram_size= @save_histogram_size; diff --git a/sql/field.cc b/sql/field.cc index 590253ac49bdd..680a337b77177 100644 --- a/sql/field.cc +++ b/sql/field.cc @@ -11833,11 +11833,14 @@ void Field_blob::print_key_value(String *out, uint32 length) key value of the key length Length of field in bytes, excluding NULL flag and length bytes + image_type Form in which the value is stored in the key: + itRAW for regular indexes, itMBR for SPATIAL indexes */ void -Field::print_key_part_value(String *out, const uchar* key, uint32 length) +Field::print_key_part_value(String *out, const uchar* key, uint32 length, + imagetype image_type) { StringBuffer<128> tmp(system_charset_info); uint null_byte= 0; diff --git a/sql/field.h b/sql/field.h index 2797cad5da427..e09619b76c1d9 100644 --- a/sql/field.h +++ b/sql/field.h @@ -1759,7 +1759,8 @@ class Field: public Value_source bool set_warning(Sql_condition::enum_warning_level, unsigned int code, int cuted_increment, ulong current_row=0) const; virtual void print_key_value(String *out, uint32 length); - void print_key_part_value(String *out, const uchar *key, uint32 length); + virtual void print_key_part_value(String *out, const uchar *key, uint32 length, + imagetype image_type); void print_key_value_binary(String *out, const uchar* key, uint32 length); void raise_note_cannot_use_key_part(THD *thd, uint keynr, uint part, const LEX_CSTRING &op, diff --git a/sql/opt_context_store_replay.cc b/sql/opt_context_store_replay.cc index c89933fcc71c9..3b79b86154e48 100644 --- a/sql/opt_context_store_replay.cc +++ b/sql/opt_context_store_replay.cc @@ -1130,8 +1130,12 @@ void Optimizer_context_recorder::record_records_in_range( rec_in_range_ctx->keynr= keynr; String min_key; String max_key; - print_key_value(&min_key, key_part, min_range->key, min_range->length); - print_key_value(&max_key, key_part, max_range->key, max_range->length); + Field::imagetype image_type= + Field::image_type(tbl->key_info[keynr].algorithm); + print_key_value(&min_key, key_part, min_range->key, min_range->length, + image_type); + print_key_value(&max_key, key_part, max_range->key, max_range->length, + image_type); if (!(rec_in_range_ctx->min_key= strdup_root(mem_root, &min_key))) return; // OOM @@ -1965,8 +1969,12 @@ bool Optimizer_context_replay::infuse_records_in_range( String min_key; String max_key; String tbl_name; - print_key_value(&min_key, key_part, min_range->key, min_range->length); - print_key_value(&max_key, key_part, max_range->key, max_range->length); + Field::imagetype image_type= + Field::image_type(tbl->key_info[keynr].algorithm); + print_key_value(&min_key, key_part, min_range->key, min_range->length, + image_type); + print_key_value(&max_key, key_part, max_range->key, max_range->length, + image_type); append_base_table_name(tbl, &tbl_name); if (table_context_for_replay *tbl_ctx= diff --git a/sql/opt_range.cc b/sql/opt_range.cc index ab003b70c5afd..822a0736ad198 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -17595,6 +17595,43 @@ static void print_max_range_operator(String *out, const ha_rkey_function flag) out->append(STRING_WITH_LEN(" ? ")); } +/* + @brief Print the spatial relation used by a range over a SPATIAL index + + @detail + The relation is printed the way it reads with the indexed column on the + left side, that is, the range + (g) mbrwithin (POLYGON(...)) + reads rows whose MBR is within the MBR of the printed value. + Note that mbrtouches/mbrcrosses/mbroverlaps all use the same + HA_READ_MBR_INTERSECT relation, so they are all printed as mbrintersects. +*/ + +static void print_mbr_range_operator(String *out, const ha_rkey_function flag) +{ + switch (flag) + { + case HA_READ_MBR_CONTAIN: + out->append(STRING_WITH_LEN(" MBRWITHIN ")); + break; + case HA_READ_MBR_WITHIN: + out->append(STRING_WITH_LEN(" MBRCONTAINS ")); + break; + case HA_READ_MBR_INTERSECT: + out->append(STRING_WITH_LEN(" MBRINTERSECTS ")); + break; + case HA_READ_MBR_DISJOINT: + out->append(STRING_WITH_LEN(" MBRDISJOINT ")); + break; + case HA_READ_MBR_EQUAL: + out->append(STRING_WITH_LEN(" MBREQUALS ")); + break; + default: + out->append(STRING_WITH_LEN(" ? ")); + break; + } +} + void print_range(String *out, const KEY_PART_INFO *key_part, KEY_MULTI_RANGE *range, uint n_key_parts) { @@ -17608,21 +17645,22 @@ void print_range(String *out, const KEY_PART_INFO *key_part, if (flag & GEOM_FLAG) { /* - The flags of GEOM ranges do not work the same way as for other - range types, so printing "col < some_geom" doesn't make sense. - Just print the column name, not operator. + The flags of GEOM ranges do not work the same way as for other range + types: there is no min/max bound, the range is the set of rows whose + MBR has the given spatial relation with the MBR of the value. + Print it as "col mbr_relation value". */ print_keyparts_name(out, key_part, n_key_parts, keypart_map); - out->append(STRING_WITH_LEN(" ")); + print_mbr_range_operator(out, range->start_key.flag); print_key_value(out, key_part, range->start_key.key, - range->start_key.length); + range->start_key.length, Field::itMBR); return; } if (range->start_key.length) { print_key_value(out, key_part, range->start_key.key, - range->start_key.length); + range->start_key.length, Field::itRAW); print_min_range_operator(out, range->start_key.flag); } @@ -17631,8 +17669,8 @@ void print_range(String *out, const KEY_PART_INFO *key_part, if (range->end_key.length) { print_max_range_operator(out, range->end_key.flag); - print_key_value(out, key_part, range->end_key.key, - range->end_key.length); + print_key_value(out, key_part, range->end_key.key, range->end_key.length, + Field::itRAW); } } @@ -17656,7 +17694,8 @@ void print_range_for_non_indexed_field(String *out, Field *field, if (range->start_key.length) { - field->print_key_part_value(out, range->start_key.key, field->key_length()); + field->print_key_part_value(out, range->start_key.key, field->key_length(), + Field::itRAW); print_min_range_operator(out, range->start_key.flag); } @@ -17665,7 +17704,8 @@ void print_range_for_non_indexed_field(String *out, Field *field, if (range->end_key.length) { print_max_range_operator(out, range->end_key.flag); - field->print_key_part_value(out, range->end_key.key, field->key_length()); + field->print_key_part_value(out, range->end_key.key, field->key_length(), + Field::itRAW); } dbug_tmp_restore_column_maps(&table->read_set, &table->write_set, old_sets); } @@ -17703,10 +17743,13 @@ static void trace_ranges(Json_writer_array *range_trace, PARAM *param, @param[in] key_part Index components description @param[in] key Key tuple @param[in] used_length length of the key tuple + @param[in] image_type Form in which the key stores the column values: + itRAW for regular indexes, itMBR for SPATIAL ones */ void print_key_value(String *out, const KEY_PART_INFO *key_part, - const uchar *key, uint used_length) + const uchar *key, uint used_length, + Field::imagetype image_type) { out->append(STRING_WITH_LEN("(")); Field *field= key_part->field; @@ -17722,7 +17765,7 @@ void print_key_value(String *out, const KEY_PART_INFO *key_part, field= key_part->field; store_length= key_part->store_length; - field->print_key_part_value(out, key, key_part->length); + field->print_key_part_value(out, key, key_part->length, image_type); if (key + store_length < key_end) out->append(STRING_WITH_LEN(",")); diff --git a/sql/opt_range.h b/sql/opt_range.h index 50cbc27c67f28..343d49f10f3d6 100644 --- a/sql/opt_range.h +++ b/sql/opt_range.h @@ -2044,7 +2044,8 @@ void print_range(String *out, const KEY_PART_INFO *key_part, KEY_MULTI_RANGE *range, uint n_key_parts); void print_key_value(String *out, const KEY_PART_INFO *key_part, - const uchar *key, uint used_length); + const uchar *key, uint used_length, + Field::imagetype image_type); #ifdef WITH_PARTITION_STORAGE_ENGINE bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond); diff --git a/sql/sql_type_geom.cc b/sql/sql_type_geom.cc index d18c6d6194d29..e7b10a4344014 100644 --- a/sql/sql_type_geom.cc +++ b/sql/sql_type_geom.cc @@ -1001,6 +1001,109 @@ uint Field_geom::get_key_image(uchar *buff,uint length, const uchar *ptr_arg, return Field_blob::get_key_image_itRAW(ptr_arg, buff, length); } +/* + @brief + Print the geometry value stored in this field in the WKT format. + + @detail + This is used to print index key values in the optimizer trace and in the + Optimizer Context. The value comes from an index, so it may be just a + prefix of the original column value (for indexes like KEY(g(10))). + Such a truncated value cannot be converted to WKT, print it as a + binary string then. +*/ + +void Field_geom::print_key_value(String *out, uint32 length) +{ + Geometry_buffer buffer; + Geometry *geom; + const char *dummy; + const uchar *wkb= get_ptr(); + uint32 wkb_length= get_length(); + StringBuffer<128> wkt(&my_charset_latin1); + + if ((geom= Geometry::construct(&buffer, (const char *) wkb, wkb_length)) && + geom->is_binary_valid() && + !geom->as_wkt(&wkt, &dummy)) + out->append(wkt.ptr(), wkt.length(), wkt.charset()); + else + print_key_value_binary(out, wkb, wkb_length); +} + + +/* + @brief + Print the value of a key part over a GEOMETRY column. + + @detail + SPATIAL indexes do not store the column value. They store its MBR + (Minimum Bounding Rectangle) instead, as four doubles: + xmin, xmax, ymin, ymax + Print the MBR as a WKT POLYGON, the same way ST_Envelope() would + return it. + For the other index types the key holds the value (or its prefix), + which is printed by Field_geom::print_key_value(). +*/ + +void Field_geom::print_key_part_value(String *out, const uchar *key, + uint32 length, imagetype image_type) +{ + if (image_type != itMBR) + { + Field_blob::print_key_part_value(out, key, length, image_type); + return; + } + + if (real_maybe_null()) + { + /* + SPATIAL keys do not support NULL, but be graceful here as this is + only used for printing. + */ + if (*key) + { + out->append(NULL_clex_str); + return; + } + key++; // Skip the null byte + } + + if (length < SIZEOF_STORED_DOUBLE * 4) + { + /* + The key part is too short to hold a full MBR (four doubles). This should + not happen for a SPATIAL index, but be graceful and print whatever bytes + are there as a binary string instead of reading past the key. + TODO: Should we assert? + */ + print_key_value_binary(out, key, length); + return; + } + + double xmin, xmax, ymin, ymax; + float8get(xmin, key); + float8get(xmax, key + SIZEOF_STORED_DOUBLE); + float8get(ymin, key + SIZEOF_STORED_DOUBLE * 2); + float8get(ymax, key + SIZEOF_STORED_DOUBLE * 3); + + if (out->reserve(MY_GCVT_MAX_FIELD_WIDTH * 10 + 32)) + return; // Out of memory + + out->qs_append(STRING_WITH_LEN("POLYGON((")); + const double points[5][2]= {{xmin, ymin}, {xmax, ymin}, {xmax, ymax}, + {xmin, ymax}, {xmin, ymin}}; + for (uint i= 0; i < array_elements(points); i++) + { + if (i) + out->qs_append(','); + out->qs_append(points[i][0]); + out->qs_append(' '); + out->qs_append(points[i][1]); + } + out->qs_append(STRING_WITH_LEN("))")); +} + + Binlog_type_info Field_geom::binlog_type_info() const { DBUG_ASSERT(Field_geom::type() == binlog_type()); diff --git a/sql/sql_type_geom.h b/sql/sql_type_geom.h index 9dc4a55edaa46..d0c31c632e249 100644 --- a/sql/sql_type_geom.h +++ b/sql/sql_type_geom.h @@ -463,10 +463,9 @@ class Field_geom :public Field_blob, bool load_data_set_null(THD *thd) override; bool load_data_set_no_data(THD *thd, bool fixed_format) override; - void print_key_value(String *out, uint32 length) override - { - out->append(STRING_WITH_LEN("unprintable_geometry_value")); - } + void print_key_value(String *out, uint32 length) override; + void print_key_part_value(String *out, const uchar *key, uint32 length, + imagetype image_type) override; Binlog_type_info binlog_type_info() const override; };