Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pushing AsOf expressions down to tables used in unions and subqueries in view definitions #1159

Merged
merged 11 commits into from
Aug 8, 2022
48 changes: 28 additions & 20 deletions enginetest/enginetests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1667,15 +1667,11 @@ func TestViewsPrepared(t *testing.T, harness Harness) {
}
}

func TestVersionedViews(t *testing.T, harness Harness) {
if _, ok := harness.(VersionedDBHarness); !ok {
t.Skipf("Skipping versioned test, harness doesn't implement VersionedDBHarness")
}

// initializeViewsForVersionedViewsTests creates the test views used by the TestVersionedViews and
// TestVersionedViewsPrepared functions.
func initializeViewsForVersionedViewsTests(t *testing.T, harness Harness, e *sqle.Engine) {
require := require.New(t)

e := NewEngine(t, harness)
defer e.Close()
ctx := NewContext(harness)
_, iter, err := e.Query(ctx, "CREATE VIEW myview1 AS SELECT * FROM myhistorytable")
require.NoError(err)
Expand All @@ -1686,9 +1682,31 @@ func TestVersionedViews(t *testing.T, harness Harness) {
require.NoError(err)
iter.Close(ctx)

// views with unions
_, iter, err = e.Query(ctx, "CREATE VIEW myview3 AS SELECT i from myview1 union select s from myhistorytable")
require.NoError(err)
iter.Close(ctx)

// views with subqueries
_, iter, err = e.Query(ctx, "CREATE VIEW myview4 AS SELECT * FROM myhistorytable where i in (select distinct cast(RIGHT(s, 1) as signed) from myhistorytable)")
fulghum marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(err)
iter.Close(ctx)
}

func TestVersionedViews(t *testing.T, harness Harness) {
if _, ok := harness.(VersionedDBHarness); !ok {
t.Skipf("Skipping versioned test, harness doesn't implement VersionedDBHarness")
}

e := NewEngine(t, harness)
defer e.Close()

initializeViewsForVersionedViewsTests(t, harness, e)
for _, testCase := range queries.VersionedViewTests {
ctx := NewContext(harness)
TestQueryWithContext(t, ctx, e, harness, testCase.Query, testCase.Expected, testCase.ExpectedColumns, nil)
t.Run(testCase.Query, func(t *testing.T) {
ctx := NewContext(harness)
TestQueryWithContext(t, ctx, e, harness, testCase.Query, testCase.Expected, testCase.ExpectedColumns, nil)
})
}
}

Expand All @@ -1697,20 +1715,10 @@ func TestVersionedViewsPrepared(t *testing.T, harness Harness) {
t.Skipf("Skipping versioned test, harness doesn't implement VersionedDBHarness")
}

require := require.New(t)

e := NewEngine(t, harness)
defer e.Close()
ctx := NewContext(harness)
_, iter, err := e.Query(ctx, "CREATE VIEW myview1 AS SELECT * FROM myhistorytable")
require.NoError(err)
iter.Close(ctx)

// nested views
_, iter, err = e.Query(ctx, "CREATE VIEW myview2 AS SELECT * FROM myview1 WHERE i = 1")
require.NoError(err)
iter.Close(ctx)

initializeViewsForVersionedViewsTests(t, harness, e)
for _, testCase := range queries.VersionedViewTests {
TestPreparedQueryWithEngine(t, harness, e, testCase)
}
Expand Down
60 changes: 60 additions & 0 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -8838,13 +8838,71 @@ var VersionedViewTests = []QueryTest{
sql.NewRow(int64(1), "first row, 1"),
},
},

// Views with unions
{
Query: "SELECT * FROM myview3 AS OF '2019-01-01'",
Expected: []sql.Row{
{"1"},
{"2"},
{"3"},
{"first row, 1"},
{"second row, 1"},
{"third row, 1"},
},
},
{
Query: "SELECT * FROM myview3 AS OF '2019-01-02'",
Expected: []sql.Row{
{"1"},
{"2"},
{"3"},
{"first row, 2"},
{"second row, 2"},
{"third row, 2"},
},
},
{
Query: "SELECT * FROM myview3 AS OF '2019-01-03'",
Expected: []sql.Row{
{"1"},
{"2"},
{"3"},
{"first row, 3"},
{"second row, 3"},
{"third row, 3"},
},
},

// Views with subqueries
{
Query: "SELECT * FROM myview4 AS OF '2019-01-01'",
Expected: []sql.Row{
{1, "first row, 1"},
},
},
{
Query: "SELECT * FROM myview4 AS OF '2019-01-02'",
Expected: []sql.Row{
{2, "second row, 2"},
},
},
{
Query: "SELECT * FROM myview4 AS OF '2019-01-03'",
Expected: []sql.Row{
{3, "third row, 3", "3"},
},
},

// info schema support
{
Query: "select * from information_schema.views where table_schema = 'mydb'",
Expected: []sql.Row{
sql.NewRow("def", "mydb", "myview", "SELECT * FROM mytable", "NONE", "YES", "", "DEFINER", "utf8mb4", "utf8mb4_0900_bin"),
sql.NewRow("def", "mydb", "myview1", "SELECT * FROM myhistorytable", "NONE", "YES", "", "DEFINER", "utf8mb4", "utf8mb4_0900_bin"),
sql.NewRow("def", "mydb", "myview2", "SELECT * FROM myview1 WHERE i = 1", "NONE", "YES", "", "DEFINER", "utf8mb4", "utf8mb4_0900_bin"),
sql.NewRow("def", "mydb", "myview3", "SELECT i from myview1 union select s from myhistorytable", "NONE", "YES", "", "DEFINER", "utf8mb4", "utf8mb4_0900_bin"),
sql.NewRow("def", "mydb", "myview4", "SELECT * FROM myhistorytable where i in (select distinct cast(RIGHT(s, 1) as signed) from myhistorytable)", "NONE", "YES", "", "DEFINER", "utf8mb4", "utf8mb4_0900_bin"),
},
},
{
Expand All @@ -8853,6 +8911,8 @@ var VersionedViewTests = []QueryTest{
sql.NewRow("myview"),
sql.NewRow("myview1"),
sql.NewRow("myview2"),
sql.NewRow("myview3"),
sql.NewRow("myview4"),
},
},
}
Expand Down
45 changes: 44 additions & 1 deletion sql/analyzer/resolve_views.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,53 @@ func resolveViews(ctx *sql.Context, a *Analyzer, n sql.Node, scope *Scope, sel R
})
}

// applyAsOfToView transforms the nodes in the view's execution plan to apply the asOf expression to every
// individual table involved in the view.
func applyAsOfToView(n sql.Node, a *Analyzer, asOf sql.Expression) (sql.Node, transform.TreeIdentity, error) {
a.Log("applying AS OF clause to view definition")

return transform.Node(n, func(n sql.Node) (sql.Node, transform.TreeIdentity, error) {
// Transform any tables in our node tree so that they use the AsOf expression
newNode, nodeIdentity, err := applyAsOfToViewTables(n, a, asOf)
if err != nil {
return n, transform.SameTree, err
}

// Subquery expressions won't get updated by the Node transform above, but we still need to update
// any UnresolvedTable references in them to set the AsOf expression correctly.
newNode, exprIdentity, err := applyAsOfToViewSubqueries(newNode, a, asOf)
fulghum marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return n, transform.SameTree, err
}

identity := transform.SameTree
if exprIdentity == transform.NewTree || nodeIdentity == transform.NewTree {
identity = transform.NewTree
}

return newNode, identity, nil
}

// applyAsOfToViewSubqueries transforms the specified node by traversing its expressions, finding all the subquery expressions,
// and running applyAsOfToViewTables on each subquery's query node.
func applyAsOfToViewSubqueries(n sql.Node, a *Analyzer, asOf sql.Expression) (sql.Node, transform.TreeIdentity, error) {
return transform.NodeExprsWithNode(n, func(node sql.Node, expression sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
if sq, ok := expression.(*plan.Subquery); ok {
newNode, identity, err := applyAsOfToViewTables(sq.Query, a, asOf)
if err != nil {
return expression, transform.SameTree, err
}
if identity == transform.NewTree {
return sq.WithQuery(newNode), transform.NewTree, nil
}
}

return expression, transform.SameTree, nil
})
}

// applyAsOfToViewTables transforms the specified node by
func applyAsOfToViewTables(newNode sql.Node, a *Analyzer, asOf sql.Expression) (sql.Node, transform.TreeIdentity, error) {
return transform.NodeWithOpaque(newNode, func(n sql.Node) (sql.Node, transform.TreeIdentity, error) {
urt, ok := n.(*plan.UnresolvedTable)
if !ok {
return n, transform.SameTree, nil
Expand Down
86 changes: 58 additions & 28 deletions sql/analyzer/resolve_views_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,56 +26,86 @@ import (
"github.com/dolthub/go-mysql-server/sql/plan"
)

func TestResolveViews(t *testing.T) {
require := require.New(t)
var viewDefinition = plan.NewSubqueryAlias(
"myview1", "select i from mytable",
plan.NewProject(
[]sql.Expression{expression.NewUnresolvedColumn("i")},
plan.NewUnresolvedTable("mytable", ""),
),
)

f := getRule(resolveViewsId)
var viewDefinitionWithUnion = plan.NewSubqueryAlias(
"myview2", "select i from mytable1 union select i from mytable2",
plan.NewProject(
[]sql.Expression{expression.NewUnresolvedColumn("i")},
plan.NewUnion(
plan.NewUnresolvedTable("mytable1", ""),
plan.NewUnresolvedTable("mytable2", "")),
),
)

viewDefinition := plan.NewSubqueryAlias(
"myview", "select i from mytable",
plan.NewProject(
[]sql.Expression{expression.NewUnresolvedColumn("i")},
plan.NewUnresolvedTable("mytable", ""),
),
)
view := sql.NewView("myview", viewDefinition, "select i from mytable")
var viewDefinitionWithAsOf = plan.NewSubqueryAlias(
"viewWithAsOf", "select i from mytable as of '2019-01-01'",
plan.NewProject(
[]sql.Expression{expression.NewUnresolvedColumn("i")},
plan.NewUnresolvedTableAsOf("mytable", "", expression.NewLiteral("2019-01-01", sql.LongText)),
),
)

func TestResolveViews(t *testing.T) {
// Initialize views and DB
view := sql.NewView(viewDefinition.Name(), viewDefinition, viewDefinition.TextDefinition)
viewWithUnion := sql.NewView(viewDefinitionWithUnion.Name(), viewDefinitionWithUnion, viewDefinitionWithUnion.TextDefinition)
viewWithAsOf := sql.NewView(viewDefinitionWithAsOf.Name(), viewDefinitionWithAsOf, viewDefinitionWithAsOf.TextDefinition)

db := memory.NewDatabase("mydb")
viewReg := sql.NewViewRegistry()
err := viewReg.Register(db.Name(), view)
require.NoError(err)
require.NoError(t, viewReg.Register(db.Name(), view))
require.NoError(t, viewReg.Register(db.Name(), viewWithUnion))
require.NoError(t, viewReg.Register(db.Name(), viewWithAsOf))

f := getRule(resolveViewsId)
a := NewBuilder(sql.NewDatabaseProvider(db)).AddPostAnalyzeRule(f.Id, f.Apply).Build()

sess := sql.NewBaseSession()
sess.SetViewRegistry(viewReg)
ctx := sql.NewContext(context.Background(), sql.WithSession(sess)).WithCurrentDB("mydb")

// AS OF expressions on a view should be pushed down to unresolved tables
var notAnalyzed sql.Node = plan.NewUnresolvedTable("myview", "")
var notAnalyzed sql.Node = plan.NewUnresolvedTable("myview1", "")
analyzed, _, err := f.Apply(ctx, a, notAnalyzed, nil, DefaultRuleSelector)
require.NoError(err)
require.Equal(viewDefinition, analyzed)

viewDefinitionWithAsOf := plan.NewSubqueryAlias(
"myview", "select i from mytable",
require.NoError(t, err)
require.Equal(t, viewDefinition, analyzed)
expectedViewDefinition := plan.NewSubqueryAlias(
"myview1", "select i from mytable",
plan.NewProject(
[]sql.Expression{expression.NewUnresolvedColumn("i")},
plan.NewUnresolvedTableAsOf("mytable", "", expression.NewLiteral("2019-01-01", sql.LongText)),
),
)
var notAnalyzedAsOf sql.Node = plan.NewUnresolvedTableAsOf("myview", "", expression.NewLiteral("2019-01-01", sql.LongText))
var notAnalyzedAsOf sql.Node = plan.NewUnresolvedTableAsOf("myview1", "", expression.NewLiteral("2019-01-01", sql.LongText))
analyzed, _, err = f.Apply(ctx, a, notAnalyzedAsOf, nil, DefaultRuleSelector)
require.NoError(t, err)
require.Equal(t, expectedViewDefinition, analyzed)

// Views using a union statement should have AsOf pushed to their unresolved tables, even though union is opaque
expectedViewDefinition = plan.NewSubqueryAlias(
"myview2", "select i from mytable1 union select i from mytable2",
plan.NewProject(
[]sql.Expression{expression.NewUnresolvedColumn("i")},
plan.NewUnion(
plan.NewUnresolvedTableAsOf("mytable1", "", expression.NewLiteral("2019-01-01", sql.LongText)),
plan.NewUnresolvedTableAsOf("mytable2", "", expression.NewLiteral("2019-01-01", sql.LongText))),
),
)
notAnalyzedAsOf = plan.NewUnresolvedTableAsOf("myview2", "", expression.NewLiteral("2019-01-01", sql.LongText))
analyzed, _, err = f.Apply(ctx, a, notAnalyzedAsOf, nil, DefaultRuleSelector)
require.NoError(err)
require.Equal(viewDefinitionWithAsOf, analyzed)
require.NoError(t, err)
require.Equal(t, expectedViewDefinition, analyzed)

// Views that are defined with AS OF clauses cannot have an AS OF pushed down to them
viewWithAsOf := sql.NewView("viewWithAsOf", viewDefinitionWithAsOf, "select i from mytable as of '2019-01-01'")
err = viewReg.Register(db.Name(), viewWithAsOf)
require.NoError(err)

notAnalyzedAsOf = plan.NewUnresolvedTableAsOf("viewWithAsOf", "", expression.NewLiteral("2019-01-01", sql.LongText))
analyzed, _, err = f.Apply(ctx, a, notAnalyzedAsOf, nil, DefaultRuleSelector)
require.Error(err)
require.True(sql.ErrIncompatibleAsOf.Is(err), "wrong error type")
require.Error(t, err)
require.True(t, sql.ErrIncompatibleAsOf.Is(err), "wrong error type")
}
2 changes: 1 addition & 1 deletion sql/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ func DebugString(nodeOrExpression interface{}) string {
}

// OpaqueNode is a node that doesn't allow transformations to its children and
// acts a a black box.
// acts as a black box.
type OpaqueNode interface {
Node
// Opaque reports whether the node is opaque or not.
Expand Down
2 changes: 1 addition & 1 deletion sql/transform/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
type NodeFunc func(n sql.Node) (sql.Node, TreeIdentity, error)

// ExprFunc is a function that given an expression will return that
// expression as is or transformed, a a TreeIdentity to indicate
// expression as is or transformed, a TreeIdentity to indicate
// whether the expression was modified, and an error or nil.
type ExprFunc func(e sql.Expression) (sql.Expression, TreeIdentity, error)

Expand Down