github.com/dolthub/go-mysql-server@v0.18.0/enginetest/spatial_index_tests.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package enginetest 16 17 import ( 18 "testing" 19 20 "github.com/stretchr/testify/require" 21 22 "github.com/dolthub/go-mysql-server/sql" 23 "github.com/dolthub/go-mysql-server/sql/plan" 24 "github.com/dolthub/go-mysql-server/sql/transform" 25 "github.com/dolthub/go-mysql-server/sql/types" 26 ) 27 28 type SpatialIndexPlanTestAssertion struct { 29 q string 30 skip bool 31 skipPrep bool 32 noIdx bool 33 exp []sql.Row 34 } 35 36 type SpatialIndexPlanTest struct { 37 name string 38 setup []string 39 tests []SpatialIndexPlanTestAssertion 40 } 41 42 var SpatialIndexTests = []SpatialIndexPlanTest{ 43 { 44 name: "filter point table with st_intersects", 45 setup: []string{ 46 "create table point_tbl(p point not null srid 0, spatial index (p))", 47 "insert into point_tbl values (point(0,0)), (point(1,1)), (point(2,2))", 48 }, 49 tests: []SpatialIndexPlanTestAssertion{ 50 { 51 q: "select p from point_tbl where st_intersects(p, point(0,0))", 52 exp: []sql.Row{ 53 {types.Point{}}, 54 }, 55 }, 56 }, 57 }, 58 { 59 name: "filter point table with st_intersects with Equals", 60 setup: []string{ 61 "create table point_tbl(p point not null srid 0, spatial index (p))", 62 "insert into point_tbl values (point(0,0)), (point(1,1)), (point(2,2))", 63 }, 64 tests: []SpatialIndexPlanTestAssertion{ 65 { 66 noIdx: true, // this should take advantage of indexes 67 q: "select p from point_tbl where st_intersects(p, point(0,0)) = true", 68 exp: []sql.Row{ 69 {types.Point{}}, 70 }, 71 }, 72 { 73 noIdx: true, 74 q: "select st_aswkt(p) from point_tbl where st_intersects(p, point(0,0)) = false order by st_x(p), st_y(p)", 75 exp: []sql.Row{ 76 {"POINT(1 1)"}, 77 {"POINT(2 2)"}, 78 }, 79 }, 80 }, 81 }, 82 { 83 name: "filter point table with st_intersects with ANDs and ORs", 84 setup: []string{ 85 "create table point_tbl(p point not null srid 0, spatial index (p))", 86 "insert into point_tbl values (point(0,0)), (point(1,1)), (point(2,2))", 87 "create table point_tbl_pk(pk int primary key, p point not null srid 0, spatial index (p))", 88 "insert into point_tbl_pk values (0, point(0,0)), (1, point(1,1)), (2, point(2,2))", 89 }, 90 tests: []SpatialIndexPlanTestAssertion{ 91 { 92 noIdx: true, 93 q: "select p from point_tbl where st_intersects(p, point(0,0)) and st_intersects(p, point(1,1))", 94 exp: []sql.Row{}, 95 }, 96 { 97 noIdx: true, 98 q: "select st_aswkt(p) from point_tbl where st_intersects(p, point(0,0)) or st_intersects(p, point(1,1)) order by st_x(p), st_y(p)", 99 exp: []sql.Row{ 100 {"POINT(0 0)"}, 101 {"POINT(1 1)"}, 102 }, 103 }, 104 { 105 noIdx: false, // still expect index access using primary key 106 q: "select pk, st_aswkt(p) from point_tbl_pk where pk = 0 and st_intersects(p, point(0,0)) order by pk", 107 exp: []sql.Row{ 108 {0, "POINT(0 0)"}, 109 }, 110 }, 111 { 112 noIdx: false, // still expect index access using primary key 113 q: "select pk, st_aswkt(p) from point_tbl_pk where pk = 0 or st_intersects(p, point(1,1)) order by pk", 114 exp: []sql.Row{ 115 {0, "POINT(0 0)"}, 116 {1, "POINT(1 1)"}, 117 }, 118 }, 119 }, 120 }, 121 { 122 name: "filter subquery with st_intersects", 123 setup: []string{ 124 "create table point_tbl(p point not null srid 0, spatial index (p))", 125 "insert into point_tbl values (point(0,0)), (point(1,1)), (point(2,2))", 126 }, 127 tests: []SpatialIndexPlanTestAssertion{ 128 { 129 q: "select st_aswkt(p) from (select * from point_tbl) t where st_intersects(p, point(0,0))", 130 exp: []sql.Row{ 131 {"POINT(0 0)"}, 132 }, 133 }, 134 }, 135 }, 136 { 137 name: "filter geom table with st_intersects", 138 setup: []string{ 139 "create table geom_tbl(g geometry not null srid 0, spatial index (g))", 140 "insert into geom_tbl values (point(0,0))", 141 "insert into geom_tbl values (st_geomfromtext('linestring(-1 -1,1 1)'))", 142 "insert into geom_tbl values (st_geomfromtext('polygon((2 2,2 -2,-2 -2,-2 2,2 2),(1 1,1 -1,-1 -1,-1 1,1 1))'))", 143 }, 144 tests: []SpatialIndexPlanTestAssertion{ 145 { 146 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, point(0,0)) order by g", 147 exp: []sql.Row{ 148 {"POINT(0 0)"}, 149 {"LINESTRING(-1 -1,1 1)"}, 150 }, 151 }, 152 { 153 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, linestring(point(-1,1), point(1,-1))) order by g", 154 exp: []sql.Row{ 155 {"POINT(0 0)"}, 156 {"LINESTRING(-1 -1,1 1)"}, 157 {"POLYGON((2 2,2 -2,-2 -2,-2 2,2 2),(1 1,1 -1,-1 -1,-1 1,1 1))"}, 158 }, 159 }, 160 }, 161 }, 162 { 163 name: "filter complicated geom table with st_intersects", 164 setup: []string{ 165 "create table geom_tbl(g geometry not null srid 0, spatial index (g))", 166 167 "insert into geom_tbl values (point(-2,-2))", 168 "insert into geom_tbl values (point(-2,-1))", 169 "insert into geom_tbl values (point(-2,0))", 170 "insert into geom_tbl values (point(-2,1))", 171 "insert into geom_tbl values (point(-2,2))", 172 173 "insert into geom_tbl values (point(-1,-2))", 174 "insert into geom_tbl values (point(-1,-1))", 175 "insert into geom_tbl values (point(-1,0))", 176 "insert into geom_tbl values (point(-1,1))", 177 "insert into geom_tbl values (point(-1,2))", 178 179 "insert into geom_tbl values (point(0,-2))", 180 "insert into geom_tbl values (point(0,-1))", 181 "insert into geom_tbl values (point(0,0))", 182 "insert into geom_tbl values (point(0,1))", 183 "insert into geom_tbl values (point(0,2))", 184 185 "insert into geom_tbl values (point(1,-2))", 186 "insert into geom_tbl values (point(1,-1))", 187 "insert into geom_tbl values (point(1,0))", 188 "insert into geom_tbl values (point(1,1))", 189 "insert into geom_tbl values (point(1,2))", 190 191 "insert into geom_tbl values (point(2,-2))", 192 "insert into geom_tbl values (point(2,-1))", 193 "insert into geom_tbl values (point(2,0))", 194 "insert into geom_tbl values (point(2,1))", 195 "insert into geom_tbl values (point(2,2))", 196 }, 197 tests: []SpatialIndexPlanTestAssertion{ 198 { 199 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, point(0,0)) order by g", 200 exp: []sql.Row{ 201 {"POINT(0 0)"}, 202 }, 203 }, 204 { 205 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, linestring(point(-1,1), point(1,-1))) order by st_x(g), st_y(g)", 206 exp: []sql.Row{ 207 {"POINT(-1 1)"}, 208 {"POINT(0 0)"}, 209 {"POINT(1 -1)"}, 210 }, 211 }, 212 { 213 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, st_geomfromtext('polygon((1 1,1 -1,-1 -1,-1 1,1 1))')) order by st_x(g), st_y(g)", 214 exp: []sql.Row{ 215 {"POINT(-1 -1)"}, 216 {"POINT(-1 0)"}, 217 {"POINT(-1 1)"}, 218 {"POINT(0 -1)"}, 219 {"POINT(0 0)"}, 220 {"POINT(0 1)"}, 221 {"POINT(1 -1)"}, 222 {"POINT(1 0)"}, 223 {"POINT(1 1)"}, 224 }, 225 }, 226 { 227 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, st_geomfromtext('linestring(-2 -2,2 2)')) order by st_x(g), st_y(g)", 228 exp: []sql.Row{ 229 {"POINT(-2 -2)"}, 230 {"POINT(-1 -1)"}, 231 {"POINT(0 0)"}, 232 {"POINT(1 1)"}, 233 {"POINT(2 2)"}, 234 }, 235 }, 236 { 237 q: "select st_aswkt(g) from geom_tbl where st_intersects(g, st_geomfromtext('multipoint(-2 -2,0 0,2 2)')) order by st_x(g), st_y(g)", 238 exp: []sql.Row{ 239 {"POINT(-2 -2)"}, 240 {"POINT(0 0)"}, 241 {"POINT(2 2)"}, 242 }, 243 }, 244 { 245 noIdx: true, 246 q: "select st_aswkt(g) from geom_tbl where not st_intersects(g, st_geomfromtext('multipoint(0 0)')) order by st_x(g), st_y(g)", 247 exp: []sql.Row{ 248 {"POINT(-2 -2)"}, 249 {"POINT(-2 -1)"}, 250 {"POINT(-2 0)"}, 251 {"POINT(-2 1)"}, 252 {"POINT(-2 2)"}, 253 {"POINT(-1 -2)"}, 254 {"POINT(-1 -1)"}, 255 {"POINT(-1 0)"}, 256 {"POINT(-1 1)"}, 257 {"POINT(-1 2)"}, 258 {"POINT(0 -2)"}, 259 {"POINT(0 -1)"}, 260 {"POINT(0 1)"}, 261 {"POINT(0 2)"}, 262 {"POINT(1 -2)"}, 263 {"POINT(1 -1)"}, 264 {"POINT(1 0)"}, 265 {"POINT(1 1)"}, 266 {"POINT(1 2)"}, 267 {"POINT(2 -2)"}, 268 {"POINT(2 -1)"}, 269 {"POINT(2 0)"}, 270 {"POINT(2 1)"}, 271 {"POINT(2 2)"}, 272 }, 273 }, 274 }, 275 }, 276 { 277 name: "negated filter point table with st_intersects does not use index", 278 setup: []string{ 279 "create table point_tbl(p point not null srid 0, spatial index (p))", 280 "insert into point_tbl values (point(0,0)), (point(1,1)), (point(2,2))", 281 }, 282 tests: []SpatialIndexPlanTestAssertion{ 283 { 284 noIdx: true, 285 q: "select st_aswkt(p) from point_tbl where not st_intersects(p, point(0,0)) order by p", 286 exp: []sql.Row{ 287 {"POINT(2 2)"}, 288 {"POINT(1 1)"}, 289 }, 290 }, 291 }, 292 }, 293 { 294 name: "filter join with st_intersects", 295 setup: []string{ 296 "create table t1(g geometry not null srid 0, spatial index (g))", 297 "create table t2(g geometry not null srid 0, spatial index (g))", 298 "insert into t1 values (point(0,0)), (point(1,1))", 299 "insert into t2 values (point(0,0)), (point(1,1))", 300 }, 301 tests: []SpatialIndexPlanTestAssertion{ 302 { 303 q: "select st_aswkt(t1.g), st_aswkt(t2.g) from t1 join t2 where st_intersects(t1.g, point(0,0))", 304 exp: []sql.Row{ 305 {"POINT(0 0)", "POINT(0 0)"}, 306 {"POINT(0 0)", "POINT(1 1)"}, 307 }, 308 }, 309 { 310 noIdx: true, // TODO: this should be able to take advantage of indexes 311 q: "select st_aswkt(t1.g), st_aswkt(t2.g) from t1 join t2 where st_intersects(t1.g, t2.g)", 312 exp: []sql.Row{ 313 {"POINT(0 0)", "POINT(0 0)"}, 314 {"POINT(1 1)", "POINT(1 1)"}, 315 }, 316 }, 317 }, 318 }, 319 { 320 name: "filter point table with st_within", 321 setup: []string{ 322 "create table point_tbl(p point not null srid 0, spatial index (p))", 323 "insert into point_tbl values (point(0,0)), (point(1,1)), (point(2,2))", 324 "create table point_pk_tbl(i int primary key, p point not null srid 0, spatial index (p))", 325 "insert into point_pk_tbl values (0, point(0,0)), (1, point(1,1)), (2, point(2,2))", 326 }, 327 tests: []SpatialIndexPlanTestAssertion{ 328 { 329 q: "select p from point_tbl where st_within(p, point(0,0))", 330 exp: []sql.Row{ 331 {types.Point{X: 0, Y: 0}}, 332 }, 333 }, 334 { 335 noIdx: true, 336 q: "select p from point_tbl where st_within(p, null)", 337 exp: []sql.Row{}, 338 }, 339 { 340 q: "select i, p from point_pk_tbl where st_within(p, point(0,0))", 341 exp: []sql.Row{ 342 {0, types.Point{X: 0, Y: 0}}, 343 }, 344 }, 345 { 346 noIdx: true, 347 q: "select i, p from point_pk_tbl where st_within(p, null)", 348 exp: []sql.Row{}, 349 }, 350 }, 351 }, 352 } 353 354 func TestSpatialIndexPlans(t *testing.T, harness Harness) { 355 for _, tt := range SpatialIndexTests { 356 t.Run(tt.name, func(t *testing.T) { 357 e := mustNewEngine(t, harness) 358 defer e.Close() 359 for _, statement := range tt.setup { 360 if sh, ok := harness.(SkippingHarness); ok { 361 if sh.SkipQueryTest(statement) { 362 t.Skip() 363 } 364 } 365 ctx := NewContext(harness) 366 RunQueryWithContext(t, e, harness, ctx, statement) 367 } 368 for _, tt := range tt.tests { 369 evalSpatialIndexPlanCorrectness(t, harness, e, tt.q, tt.q, tt.exp, tt.skip) 370 if !IsServerEngine(e) { 371 evalSpatialIndexPlanTest(t, harness, e, tt.q, tt.skip, tt.noIdx) 372 } 373 } 374 }) 375 } 376 } 377 378 func evalSpatialIndexPlanTest(t *testing.T, harness Harness, e QueryEngine, query string, skip, noIdx bool) { 379 t.Run(query+" index plan", func(t *testing.T) { 380 if skip { 381 t.Skip() 382 } 383 ctx := NewContext(harness) 384 ctx = ctx.WithQuery(query) 385 386 a, err := analyzeQuery(ctx, e, query) 387 require.NoError(t, err) 388 389 hasFilter, hasIndex, hasRightOrder := false, false, false 390 transform.Inspect(a, func(n sql.Node) bool { 391 if n == nil { 392 return false 393 } 394 if _, ok := n.(*plan.Filter); ok { 395 hasFilter = true 396 } 397 if _, ok := n.(*plan.IndexedTableAccess); ok { 398 hasRightOrder = hasFilter 399 hasIndex = true 400 } 401 return true 402 }) 403 404 require.True(t, hasFilter, "filter node was missing from plan") 405 if noIdx { 406 require.False(t, hasIndex, "indextableaccess should not be in plan") 407 } else { 408 require.True(t, hasIndex, "indextableaccess node was missing from plan:\n %s", sql.DebugString(a)) 409 require.True(t, hasRightOrder, "filter node was not above indextableaccess") 410 } 411 }) 412 } 413 414 func evalSpatialIndexPlanCorrectness(t *testing.T, harness Harness, e QueryEngine, name, q string, exp []sql.Row, skip bool) { 415 t.Run(name, func(t *testing.T) { 416 if skip { 417 t.Skip() 418 } 419 420 ctx := NewContext(harness) 421 ctx = ctx.WithQuery(q) 422 423 sch, iter, err := e.QueryWithBindings(ctx, q, nil, nil) 424 require.NoError(t, err, "Unexpected error for q %s: %s", q, err) 425 426 rows, err := sql.RowIterToRows(ctx, iter) 427 require.NoError(t, err, "Unexpected error for q %s: %s", q, err) 428 429 if exp != nil { 430 checkResults(t, exp, nil, sch, rows, q, e) 431 } 432 433 require.Equal(t, 0, ctx.Memory.NumCaches()) 434 validateEngine(t, ctx, harness, e) 435 }) 436 }