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  }