Commit b9242838 authored by craig[bot]'s avatar craig[bot]

Merge #34531 #34592

34531: ui: implement logical plan design r=celiala a=celiala

This commit implements the designs as specified in
Zeplin design "Logical Plans - v1":
https://app.zeplin.io/project/5c40e9dab2616abf79e3eb99/screen/5c4b6f23143de2bfcb12ae4e

Note that remaining design items will be addressed in future commits (tracked in #34545).

![logical-plans](https://user-images.githubusercontent.com/3051672/52312291-11070d80-2978-11e9-8ecc-42ab4a859371.gif)

Release note: None

34592: opt: Fix panic in SQLSmith query r=andy-kimball a=andy-kimball

A SQLSmith query exposed a bug in the expression interner. This bug caused
the interner to consider empty literal arrays to be equal, even if their
static types are different. For example:

  ARRAY[]:::string[]
  ARRAY[]:::int[]

An empty string array should not be treated as the same as an empty int
array. The fix is to consult the static type of the array in addition to the
types of its elements (but only when there are zero elements or one of the
elements is null). In addition, I took the opportunity to make the existing
Tuple interner code faster, by only comparing static types when lables or
nulls are present.

Fixes #34439

Release note: None
Co-authored-by: default avatarCelia La <[email protected]>
Co-authored-by: default avatarAndrew Kimball <[email protected]>
......@@ -363,16 +363,32 @@ func (h *hasher) HashDatum(val tree.Datum) {
case *tree.DJSON:
h.HashString(t.String())
case *tree.DTuple:
for _, d := range t.D {
h.HashDatum(d)
}
h.HashDatumType(t.ResolvedType())
// If labels are present, then hash of tuple's static type is needed to
// disambiguate when everything is the same except labels.
alwaysHashType := len(t.ResolvedType().(types.TTuple).Labels) != 0
h.hashDatumsWithType(t.D, t.ResolvedType(), alwaysHashType)
case *tree.DArray:
for _, d := range t.Array {
h.HashDatum(d)
}
// If the array is empty, then hash of tuple's static type is needed to
// disambiguate.
alwaysHashType := len(t.Array) == 0
h.hashDatumsWithType(t.Array, t.ResolvedType(), alwaysHashType)
default:
h.HashBytes(encodeDatum(h.bytes[:0], val))
h.bytes = encodeDatum(h.bytes[:0], val)
h.HashBytes(h.bytes)
}
}
func (h *hasher) hashDatumsWithType(datums tree.Datums, typ types.T, alwaysHashType bool) {
for _, d := range datums {
if d == tree.DNull {
// At least one NULL exists, so need to compare static types (e.g. a
// NULL::int is indistinguishable from NULL::string).
alwaysHashType = true
}
h.HashDatum(d)
}
if alwaysHashType {
h.HashDatumType(typ)
}
}
......@@ -381,9 +397,10 @@ func (h *hasher) HashDatumType(val types.T) {
}
func (h *hasher) HashColType(val coltypes.T) {
buf := bytes.NewBuffer(h.bytes)
buf := bytes.NewBuffer(h.bytes[:0])
val.Format(buf, lex.EncNoFlags)
h.HashBytes(buf.Bytes())
h.bytes = buf.Bytes()
h.HashBytes(h.bytes)
}
func (h *hasher) HashTypedExpr(val tree.TypedExpr) {
......@@ -579,11 +596,13 @@ func (h *hasher) IsDatumTypeEqual(l, r types.T) bool {
}
func (h *hasher) IsColTypeEqual(l, r coltypes.T) bool {
lbuf := bytes.NewBuffer(h.bytes)
lbuf := bytes.NewBuffer(h.bytes[:0])
l.Format(lbuf, lex.EncNoFlags)
rbuf := bytes.NewBuffer(h.bytes2)
rbuf := bytes.NewBuffer(h.bytes2[:0])
r.Format(rbuf, lex.EncNoFlags)
return bytes.Equal(lbuf.Bytes(), rbuf.Bytes())
h.bytes = lbuf.Bytes()
h.bytes2 = rbuf.Bytes()
return bytes.Equal(h.bytes, h.bytes2)
}
func (h *hasher) IsDatumEqual(l, r tree.Datum) bool {
......@@ -622,37 +641,56 @@ func (h *hasher) IsDatumEqual(l, r tree.Datum) bool {
}
case *tree.DTuple:
if rt, ok := r.(*tree.DTuple); ok {
if len(lt.D) != len(rt.D) {
// Compare datums and then compare static types if nulls or labels
// are present.
ltyp := lt.ResolvedType().(types.TTuple)
rtyp := rt.ResolvedType().(types.TTuple)
if !h.areDatumsWithTypeEqual(lt.D, rt.D, ltyp, rtyp) {
return false
}
for i := range lt.D {
if !h.IsDatumEqual(lt.D[i], rt.D[i]) {
return false
}
}
return h.IsDatumTypeEqual(l.ResolvedType(), r.ResolvedType())
return len(ltyp.Labels) == 0 || h.IsDatumTypeEqual(ltyp, rtyp)
}
case *tree.DArray:
if rt, ok := r.(*tree.DArray); ok {
if len(lt.Array) != len(rt.Array) {
// Compare datums and then compare static types if nulls are present
// or if arrays are empty.
ltyp := lt.ResolvedType()
rtyp := rt.ResolvedType()
if !h.areDatumsWithTypeEqual(lt.Array, rt.Array, ltyp, rtyp) {
return false
}
for i := range lt.Array {
if !h.IsDatumEqual(lt.Array[i], rt.Array[i]) {
return false
}
}
return true
return len(lt.Array) != 0 || h.IsDatumTypeEqual(ltyp, rtyp)
}
default:
lb := encodeDatum(h.bytes[:0], l)
rb := encodeDatum(h.bytes2[:0], r)
return bytes.Equal(lb, rb)
h.bytes = encodeDatum(h.bytes[:0], l)
h.bytes2 = encodeDatum(h.bytes2[:0], r)
return bytes.Equal(h.bytes, h.bytes2)
}
return false
}
func (h *hasher) areDatumsWithTypeEqual(ldatums, rdatums tree.Datums, ltyp, rtyp types.T) bool {
if len(ldatums) != len(rdatums) {
return false
}
foundNull := false
for i := range ldatums {
if !h.IsDatumEqual(ldatums[i], rdatums[i]) {
return false
}
if ldatums[i] == tree.DNull {
// At least one NULL exists, so need to compare static types (e.g. a
// NULL::int is indistinguishable from NULL::string).
foundNull = true
}
}
if foundNull {
return h.IsDatumTypeEqual(ltyp, rtyp)
}
return true
}
func (h *hasher) IsTypedExprEqual(l, r tree.TypedExpr) bool {
return l == r
}
......
......@@ -42,11 +42,16 @@ func TestInterner(t *testing.T) {
tupTyp2 := types.TTuple{Types: []types.T{types.Int, types.String}, Labels: []string{"a", "b"}}
tupTyp3 := types.TTuple{Types: []types.T{types.Int, types.String}}
tupTyp4 := types.TTuple{Types: []types.T{types.Int, types.String, types.Bool}}
tupTyp5 := types.TTuple{Types: []types.T{types.Int, types.String}, Labels: []string{"c", "d"}}
tupTyp6 := types.TTuple{Types: []types.T{types.String, types.Int}, Labels: []string{"c", "d"}}
tup1 := tree.NewDTuple(tupTyp1, tree.NewDInt(100), tree.NewDString("foo"))
tup2 := tree.NewDTuple(tupTyp2, tree.NewDInt(100), tree.NewDString("foo"))
tup3 := tree.NewDTuple(tupTyp3, tree.NewDInt(100), tree.NewDString("foo"))
tup4 := tree.NewDTuple(tupTyp4, tree.NewDInt(100), tree.NewDString("foo"), tree.DBoolTrue)
tup5 := tree.NewDTuple(tupTyp5, tree.NewDInt(100), tree.NewDString("foo"))
tup6 := tree.NewDTuple(tupTyp5, tree.DNull, tree.DNull)
tup7 := tree.NewDTuple(tupTyp6, tree.DNull, tree.DNull)
arr1 := tree.NewDArray(tupTyp1)
arr1.Array = tree.Datums{tup1, tup2}
......@@ -54,6 +59,14 @@ func TestInterner(t *testing.T) {
arr2.Array = tree.Datums{tup2, tup1}
arr3 := tree.NewDArray(tupTyp3)
arr3.Array = tree.Datums{tup2, tup3}
arr4 := tree.NewDArray(types.Int)
arr4.Array = tree.Datums{tree.DNull}
arr5 := tree.NewDArray(types.String)
arr5.Array = tree.Datums{tree.DNull}
arr6 := tree.NewDArray(types.Int)
arr6.Array = tree.Datums{}
arr7 := tree.NewDArray(types.String)
arr7.Array = tree.Datums{}
dec1, _ := tree.ParseDDecimal("1.0")
dec2, _ := tree.ParseDDecimal("1.0")
......@@ -187,9 +200,14 @@ func TestInterner(t *testing.T) {
{val1: tup1, val2: tup2, equal: true},
{val1: tup2, val2: tup3, equal: false},
{val1: tup3, val2: tup4, equal: false},
{val1: tup1, val2: tup5, equal: false},
{val1: tup6, val2: tup7, equal: false},
{val1: arr1, val2: arr2, equal: true},
{val1: arr2, val2: arr3, equal: false},
{val1: arr4, val2: arr5, equal: false},
{val1: arr4, val2: arr6, equal: false},
{val1: arr6, val2: arr7, equal: false},
{val1: dec1, val2: dec2, equal: true},
{val1: dec2, val2: dec3, equal: false},
......
......@@ -15,148 +15,360 @@
import { assert } from "chai";
import { cockroach } from "src/js/protos";
import { collapseRepeatedAttrs } from "src/views/statements/planView";
import ExplainTreePlanNode_IAttr = cockroach.sql.ExplainTreePlanNode.IAttr;
import { FlatPlanNode, flattenTree, planNodeHeaderProps } from "src/views/statements/planView";
import IAttr = cockroach.sql.ExplainTreePlanNode.IAttr;
import IExplainTreePlanNode = cockroach.sql.IExplainTreePlanNode;
describe("collapseRepeatedAttrs", () => {
const testAttrs1: IAttr[] = [
{
key: "key1",
value: "value1",
},
{
key: "key2",
value: "value2",
},
];
describe("when attributes array is empty", () => {
it("returns an empty result array.", () => {
const result = collapseRepeatedAttrs(makeAttrs([
]));
assert.deepEqual(result, [
]);
});
});
const testAttrs2: IAttr[] = [
{
key: "key3",
value: "value3",
},
{
key: "key4",
value: "value4",
},
];
describe("when attributes contain null properties", () => {
it("ignores attributes with null properties.", () => {
const result = collapseRepeatedAttrs([
const treePlanWithSingleChildPaths: IExplainTreePlanNode = {
name: "root",
attrs: null,
children: [
{
name: "single_grandparent",
attrs: testAttrs1,
children: [
{
key: null,
value: null,
name: "single_parent",
attrs: null,
children: [
{
name: "single_child",
attrs: testAttrs2,
children: [],
},
],
},
],
},
],
};
const expectedFlatPlanWithSingleChildPaths: FlatPlanNode[] = [
{
name: "root",
attrs: null,
children: [],
},
{
name: "single_grandparent",
attrs: testAttrs1,
children: [],
},
{
name: "single_parent",
attrs: null,
children: [],
},
{
name: "single_child",
attrs: testAttrs2,
children: [],
},
];
const treePlanWithChildren1: IExplainTreePlanNode = {
name: "root",
attrs: testAttrs1,
children: [
{
name: "single_grandparent",
attrs: testAttrs1,
children: [
{
key: "key",
value: null,
name: "parent_1",
attrs: null,
children: [
{
name: "single_child",
attrs: testAttrs2,
children: [],
},
],
},
{
key: "key",
value: "value",
name: "parent_2",
attrs: null,
children: [],
},
],
},
],
};
const expectedFlatPlanWithChildren1: FlatPlanNode[] = [
{
name: "root",
attrs: testAttrs1,
children: [],
},
{
name: "single_grandparent",
attrs: testAttrs1,
children: [
[
{
key: null,
value: "value",
name: "parent_1",
attrs: null,
children: [],
},
]);
assert.deepEqual(result, [
{
key: "key",
value: ["value"],
name: "single_child",
attrs: testAttrs2,
children: [],
},
]);
});
});
],
[
{
name: "parent_2",
attrs: null,
children: [],
},
],
],
},
];
describe("when attributes contains duplicate keys", () => {
it("groups values with same key.", () => {
const result = collapseRepeatedAttrs(makeAttrs([
"key1: value1",
"key2: value1",
"key1: value2",
"key2: value2",
"key1: value3",
]));
assert.deepEqual(result, [
const treePlanWithChildren2: IExplainTreePlanNode = {
name: "root",
attrs: null,
children: [
{
name: "single_grandparent",
attrs: null,
children: [
{
key: "key1",
value: ["value1", "value2", "value3"],
name: "single_parent",
attrs: null,
children: [
{
name: "child_1",
attrs: testAttrs1,
children: [],
},
{
name: "child_2",
attrs: testAttrs2,
children: [],
},
],
},
],
},
],
};
const expectedFlatPlanWithChildren2: FlatPlanNode[] = [
{
name: "root",
attrs: null,
children: [],
},
{
name: "single_grandparent",
attrs: null,
children: [],
},
{
name: "single_parent",
attrs: null,
children: [
[
{
name: "child_1",
attrs: testAttrs1,
children: [],
},
],
[
{
key: "key2",
value: ["value1", "value2"],
name: "child_2",
attrs: testAttrs2,
children: [],
},
]);
],
],
},
];
const treePlanWithNoChildren: IExplainTreePlanNode = {
name: "root",
attrs: testAttrs1,
children: [],
};
const expectedFlatPlanWithNoChildren: FlatPlanNode[] = [
{
name: "root",
attrs: testAttrs1,
children: [],
},
];
describe("flattenTree", () => {
describe("when node has children", () => {
it("flattens single child paths.", () => {
assert.deepEqual(
flattenTree(treePlanWithSingleChildPaths),
expectedFlatPlanWithSingleChildPaths,
);
});
it("increases level if multiple children.", () => {
assert.deepEqual(
flattenTree(treePlanWithChildren1),
expectedFlatPlanWithChildren1,
);
assert.deepEqual(
flattenTree(treePlanWithChildren2),
expectedFlatPlanWithChildren2,
);
});
});
describe("when node has no children", () => {
it("returns valid flattened plan.", () => {
assert.deepEqual(
flattenTree(treePlanWithNoChildren),
expectedFlatPlanWithNoChildren,
);
});
});
});
describe("when attribute keys are not alphabetized", () => {
describe("when no table key present", () => {
it("sorts attributes alphabetically by key.", () => {
const result = collapseRepeatedAttrs(makeAttrs([
"papaya: papaya",
"coconut: coconut",
"banana: banana",
"mango: mango",
]));
assert.deepEqual(result, [
describe("planNodeHeaderProps", () => {
describe("when node is join node", () => {
it("prepends join type to title.", () => {
const result = planNodeHeaderProps({
name: "join",
attrs: [
{
key: "banana",
value: ["banana"],
key: "foo",
value: "bar",
},
{
key: "coconut",
value: ["coconut"],
key: "type",
value: "inner",
},
{
key: "mango",
value: ["mango"],
key: "baz",
value: "foo-baz",
},
{
key: "papaya",
value: ["papaya"],
},
]);
],
children: [],
});
assert.deepEqual(
result,
{
title: "inner join",
subtitle: null,
warn: false,
},
);
});
describe("when table key is present", () => {
it("sorts attribute with table key first.", () => {
const result = collapseRepeatedAttrs(makeAttrs([
"papaya: papaya",
"coconut: coconut",
"banana: banana",
"table: table",
"mango: mango",
]));
assert.deepEqual(result, [
{
key: "table",
value: ["table"],
},
{
key: "banana",
value: ["banana"],
},
});
describe("when node is scan node", () => {
describe("if not both `spans ALL` and `table` attribute", () => {
it("returns default header properties.", () => {
const result1 = planNodeHeaderProps({
name: "scan",
attrs: [
{
key: "spans",
value: "ALL",
},
{
key: "but-not-table-key",
value: "[email protected]",
},
],
children: [],
});
const result2 = planNodeHeaderProps({
name: "scan",
attrs: [
{
key: "spans",
value: "but-not-ALL-value",
},
{
key: "table",
value: "[email protected]",
},
],
children: [],
});
assert.deepEqual(
result1,
{
key: "coconut",
value: ["coconut"],
title: "scan",
subtitle: null,
warn: false,
},
);
assert.deepEqual(
result2,
{
key: "mango",
value: ["mango"],
title: "scan",
subtitle: null,
warn: false,
},
);
});
});
describe("if both `spans ALL` and `table` attribute", () => {
it("warns user of table scan.", () => {
const result = planNodeHeaderProps({
name: "scan",
attrs: [
{
key: "foo",
value: "bar",
},
{
key: "spans",
value: "ALL",
},
{
key: "baz",
value: "foo-baz",
},
{
key: "table",
value: "[email protected]",
},
],
children: [],
});
assert.deepEqual(
result,
{
key: "papaya",
value: ["papaya"],
title: "table scan",
subtitle: "table-name",
warn: true,
},
]);
);
});
});
});
});
function makeAttrs(attributes: string[]): ExplainTreePlanNode_IAttr[] {
return attributes.map((attribute) => {
return makeAttr(attribute.split(": "));
});
}
function makeAttr(parts: string[]): ExplainTreePlanNode_IAttr {
if (parts.length < 2) {
return null;
}
return {
key: parts[0],
value: parts[1],
};
}
......@@ -16,115 +16,322 @@ import _ from "lodash";
import React from "react";
import { cockroach } from "src/js/protos";
import { intersperse } from "src/util/intersperse";
import IAttr = cockroach.sql.ExplainTreePlanNode.IAttr;
import IExplainTreePlanNode = cockroach.sql.IExplainTreePlanNode;
import ExplainTreePlanNode_IAttr = cockroach.sql.ExplainTreePlanNode.IAttr;
import { ToolTipWrapper } from "src/views/shared/components/toolTip";
export function PlanView(props: {
title: string,
plan: IExplainTreePlanNode,
}) {
if (!props.plan) {
return <div className="plan-view">
<h3>{props.title}</h3>
<p>No plan captured yet.</p>
</div>;
}
return <div className="plan-view">
<h3>{props.title}</h3>
<div className="plan-node">
<ul>
<li>
<PlanNode node={props.plan} />
</li>
</ul>
</div>
</div>;
const WARNING_ICON = (
<svg className="warning-icon" width="17" height="17" viewBox="0 0 24 22" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.7798 2.18656L23.4186 15.5005C25.0821 18.4005 22.9761 21.9972 19.6387 21.9972H4.3619C1.02395 21.9972 -1.08272 18.4009 0.582041 15.5005M0.582041 15.5005L8.21987 2.18656C9.89189 -0.728869 14.1077 -0.728837 15.7798 2.18656M13.4002 7.07075C13.4002 6.47901 12.863 5.99932 12.2002 5.99932C11.5375 5.99932 11.0002 6.47901 11.0002 7.07075V13.4993C11.0002 14.0911 11.5375 14.5707 12.2002 14.5707C12.863 14.5707 13.4002 14.0911 13.4002 13.4993V7.07075ZM13.5717 17.2774C13.5717 16.5709 12.996 15.9981 12.286 15.9981C11.5759 15.9981 11.0002 16.5709 11.0002 17.2774V17.2902C11.0002 17.9967 11.5759 18.5695 12.286 18.5695C12.996 18.5695 13.5717 17.9967 13.5717 17.2902V17.2774Z"/>
</svg>
);
const NODE_ICON = (
<span className="node-icon">&#x26AC;</span>
);
const UP_ARROW_ICON = (
<svg className="arrow-icon" viewBox="0 0 20 11" width="10" height="10">
<polyline
stroke-linecap="round"
points="2,10 10,1 18,10"
/>
</svg>
);