diff --git a/build/builder.sh b/build/builder.sh index 25bf869120ad..7c17b1b7c089 100755 --- a/build/builder.sh +++ b/build/builder.sh @@ -200,7 +200,7 @@ docker run --init --privileged -i ${tty-} --rm \ res=$? set -e -if test $res -ne 0 -a \( ${1-x} = "make" -o ${1-x} = "xmkrelease" \) ; then +if test $res -ne 0 -a \( ${1-x} = "make" -o ${1-x} = "mkrelease" \) ; then ram=$(docker run -i --rm \ -u "$uid:$gid" \ "${image}:${version}" awk '/MemTotal/{print $2}' /proc/meminfo) diff --git a/pkg/ccl/importccl/import_stmt_test.go b/pkg/ccl/importccl/import_stmt_test.go index e033edbda3cb..0bb83beabd82 100644 --- a/pkg/ccl/importccl/import_stmt_test.go +++ b/pkg/ccl/importccl/import_stmt_test.go @@ -168,14 +168,13 @@ ORDER BY table_name s string collate en_u_ks_level1 primary key `, typ: "CSV", - data: `a -B -c -D -d + data: `'a' collate en_u_ks_level1 +'B' collate en_u_ks_level1 +'c' collate en_u_ks_level1 +'D' collate en_u_ks_level1 +'d' collate en_u_ks_level1 `, - err: "duplicate key", - skipIssue: 53956, + err: "duplicate key", }, { name: "duplicate PK at sst boundary", diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planTooltips.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planTooltips.tsx new file mode 100644 index 000000000000..cac3b25802c4 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planTooltips.tsx @@ -0,0 +1,156 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import React from "react"; +import { Anchor } from "src/anchor"; +import { + distSql, + vectorizedExecution, + mergeJoin, + lookupJoin, + hashJoin, + invertedJoin, + indexJoin, + fullScan, + secondaryIndex, + lockingStrength, + transactionLayerOverview, +} from "src/util/docs"; + +type TooltipMap = Record; + +const attributeTooltips: TooltipMap = { + autoCommit: ( + + All statements are handled as transactions.{" "} + Autocommit indicates that + the single statement was implicitly committed. + + ), + distribution: ( + + CockroachDB supports{" "} + two modes of execution + for SQL queries: local and distributed. In local execution, data is pulled + from where it is towards the one node that does the processing. In + distributed execution, the processing is shipped to run close to where the + data is stored, usually on multiple nodes simultaneously. + + ), + lockingStrength: ( + + Locking strength indicates that + additional locks were applied to rows during execution to improve + performance for workloads having high contention. + + ), + vectorized: ( + + CockroachDB supports a{" "} + vectorized execution engine. + + ), +}; + +const operatorTooltips: TooltipMap = { + filter: ( + + The filter operator indicates the statement contains a WHERE clause. + + ), + group: ( + + The group operator indicates the statement contains a GROUP BY clause. + + ), + hashJoin: ( + + Hash join is used when merge join cannot + be used. CockroachDB will create a hash table based on the smaller table + and then scan the larger table, looking up each row in the hash table. + + ), + indexJoin: ( + + Index join is suboptimal and indicates a + non-covering index where not all columns are present in the index to + satisfy the query. Index join requires columns to additionally be read + from the primary index. + + ), + insertFastPath: ( + The insert operator indicates an INSERT statement. + ), + invertedJoin: ( + + Inverted join forces the optimizer to + use a join using an inverted index on the right side of the join. Inverted + joins can only be used with INNER and LEFT joins. + + ), + lookupJoin: ( + + Lookup join is used where there is a + large imbalance in size between the tables. The larger table must be + indexed on the equality column. Each row in the small table is read where + the rows are 'looked up' in the larger table for matches. + + ), + mergeJoin: ( + + Merge join is selected when both tables + are indexed on the equality columns where indexes must have the same + ordering. + + ), + scan: ( + + The scan (or reverse scan) operator indicates which and how the table(s) + in the statement's FROM clause and its index is read. + + ), + sort: ( + + The sort operator indicates the statement contains an ORDER BY clause. + + ), + update: The update operator indicates an UPDATE statement., + window: ( + + The window operator indicates the statement contains a OVER or WINDOW + clause for window functions. + + ), +}; + +const attributeValueTooltips: TooltipMap = { + fullScan: ( + + A FULL SCAN indicates that{" "} + all rows were read from the table index. +
+ Recommendation: Consider{" "} + adding a secondary index to the + table if the FULL SCAN is retrieving a relatively large number of rows. +
+ ), +}; + +export function getOperatorTooltip(key: string): React.ReactElement { + return operatorTooltips[key] || null; +} + +export function getAttributeTooltip(key: string): JSX.Element { + return attributeTooltips[key] || null; +} + +export function getAttributeValueTooltip(key: string): JSX.Element { + return attributeValueTooltips[key] || null; +} diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.module.scss b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.module.scss index a448dd3b6082..bbb2ee388c58 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.module.scss @@ -113,17 +113,34 @@ .warn { color: $colors--functional-orange-4; text-transform: uppercase; + } + + .underline-tooltip { border-bottom: 1px dashed $adminui-white; + + a { + text-decoration: underline; + font-size: inherit; + color: $chip-grey; + } } - .nodeDetails { + .node-attribute { + div { + display: inline-block; + } + } + + .node-details { position: relative; padding: 6px 0; border: 1px solid transparent; b { + div { + display: inline-block; + } font-family: $font-family--monospace; - border-bottom: 1px dashed $adminui-white; font-size: 14px; font-weight: 500; line-height: 1.67; @@ -132,24 +149,20 @@ } &:only-child { - .nodeAttributes { + .node-attributes { border-left: 1px solid transparent; } } } - .nodeAttributes.globalAttributes { + .node-attributes.global-attributes { margin-bottom: 40px; padding: 0 0 40px 0; border-left: none; border-bottom: 1px solid $colors--neutral-5; - - .nodeAttributeKey { - border-bottom: 1px dashed $adminui-white; - } } - .nodeAttributes { + .node-attributes { color: $chip-grey; padding: 3px 14px 12px 8px; margin-left: 2px; @@ -159,7 +172,11 @@ font-weight: 500; line-height: 1.83; - .nodeAttributeKey { + .node-attribute-key { + div { + display: inline-block; + } + position: relative; color: $colors--primary-blue-6; } } diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx index 17e6ca341a5a..dad1c408fbd8 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx @@ -16,6 +16,7 @@ import { FlatPlanNodeAttribute, flattenTreeAttributes, flattenAttributes, + standardizeKey, } from "./planView"; import IAttr = cockroach.sql.ExplainTreePlanNode.IAttr; @@ -73,175 +74,190 @@ const expectedTestAttrsDistinctKeys: FlatPlanNodeAttribute[] = [ }, ]; -describe("flattenTreeAttributes", () => { - describe("when all nodes have attributes with different keys", () => { - it("creates array with exactly one value for each attribute for each node", () => { - const node: IExplainTreePlanNode = { - name: "root", - attrs: testAttrsDistinctKeys, - children: [ +describe("planView", () => { + describe("flattenTreeAttributes", () => { + describe("when all nodes have attributes with different keys", () => { + it("creates array with exactly one value for each attribute for each node", () => { + const node: IExplainTreePlanNode = { + name: "root", + attrs: testAttrsDistinctKeys, + children: [ + { + name: "child", + attrs: testAttrsDistinctKeys, + children: [ + { + name: "grandchild", + attrs: testAttrsDistinctKeys, + children: [], + }, + ], + }, + ], + }; + + const nodeFlattened: FlatPlanNode = { + name: "root", + attrs: expectedTestAttrsDistinctKeys, + children: [ + { + name: "child", + attrs: expectedTestAttrsDistinctKeys, + children: [ + { + name: "grandchild", + attrs: expectedTestAttrsDistinctKeys, + children: [], + }, + ], + }, + ], + }; + + assert.deepEqual(flattenTreeAttributes(node), nodeFlattened); + }); + }); + describe("when there are nodes with multiple attributes with the same key", () => { + it("flattens attributes for each node having multiple attributes with the same key", () => { + const node: IExplainTreePlanNode = { + name: "root", + attrs: testAttrsDuplicatedKeys, + children: [ + { + name: "child", + attrs: testAttrsDuplicatedKeys, + children: [ + { + name: "grandchild", + attrs: testAttrsDuplicatedKeys, + children: [], + }, + ], + }, + ], + }; + + const nodeFlattened: FlatPlanNode = { + name: "root", + attrs: expectedTestAttrsFlattened, + children: [ + { + name: "child", + attrs: expectedTestAttrsFlattened, + children: [ + { + name: "grandchild", + attrs: expectedTestAttrsFlattened, + children: [], + }, + ], + }, + ], + }; + + assert.deepEqual(flattenTreeAttributes(node), nodeFlattened); + }); + }); + }); + + describe("flattenAttributes", () => { + describe("when all attributes have different keys", () => { + it("creates array with exactly one value for each attribute", () => { + assert.deepEqual( + flattenAttributes(testAttrsDistinctKeys), + expectedTestAttrsDistinctKeys, + ); + }); + }); + describe("when there are multiple attributes with same key", () => { + it("collects values into one array for same key", () => { + assert.deepEqual( + flattenAttributes(testAttrsDuplicatedKeys), + expectedTestAttrsFlattened, + ); + }); + }); + describe("when attribute key/value is `spans FULL SCAN`", () => { + it("sets warn to true", () => { + const testAttrs: IAttr[] = [ { - name: "child", - attrs: testAttrsDistinctKeys, - children: [ - { - name: "grandchild", - attrs: testAttrsDistinctKeys, - children: [], - }, - ], + key: "foo", + value: "bar", }, - ], - }; - - const nodeFlattened: FlatPlanNode = { - name: "root", - attrs: expectedTestAttrsDistinctKeys, - children: [ { - name: "child", - attrs: expectedTestAttrsDistinctKeys, - children: [ - { - name: "grandchild", - attrs: expectedTestAttrsDistinctKeys, - children: [], - }, - ], + key: "spans", + value: "FULL SCAN", + }, + ]; + const expectedTestAttrs: FlatPlanNodeAttribute[] = [ + { + key: "foo", + values: ["bar"], + warn: false, }, - ], - }; + { + key: "spans", + values: ["FULL SCAN"], + warn: true, + }, + ]; - assert.deepEqual(flattenTreeAttributes(node), nodeFlattened); + assert.deepEqual(flattenAttributes(testAttrs), expectedTestAttrs); + }); }); - }); - describe("when there are nodes with multiple attributes with the same key", () => { - it("flattens attributes for each node having multiple attributes with the same key", () => { - const node: IExplainTreePlanNode = { - name: "root", - attrs: testAttrsDuplicatedKeys, - children: [ + describe("when keys are unsorted", () => { + it("puts table key first, and sorts remaining keys alphabetically", () => { + const testAttrs: IAttr[] = [ { - name: "child", - attrs: testAttrsDuplicatedKeys, - children: [ - { - name: "grandchild", - attrs: testAttrsDuplicatedKeys, - children: [], - }, - ], + key: "zebra", + value: "foo", }, - ], - }; - - const nodeFlattened: FlatPlanNode = { - name: "root", - attrs: expectedTestAttrsFlattened, - children: [ { - name: "child", - attrs: expectedTestAttrsFlattened, - children: [ - { - name: "grandchild", - attrs: expectedTestAttrsFlattened, - children: [], - }, - ], + key: "table", + value: "foo", }, - ], - }; + { + key: "cheetah", + value: "foo", + }, + { + key: "table", + value: "bar", + }, + ]; + const expectedTestAttrs: FlatPlanNodeAttribute[] = [ + { + key: "table", + values: ["foo", "bar"], + warn: false, + }, + { + key: "cheetah", + values: ["foo"], + warn: false, + }, + { + key: "zebra", + values: ["foo"], + warn: false, + }, + ]; - assert.deepEqual(flattenTreeAttributes(node), nodeFlattened); + assert.deepEqual(flattenAttributes(testAttrs), expectedTestAttrs); + }); }); }); -}); -describe("flattenAttributes", () => { - describe("when all attributes have different keys", () => { - it("creates array with exactly one value for each attribute", () => { - assert.deepEqual( - flattenAttributes(testAttrsDistinctKeys), - expectedTestAttrsDistinctKeys, - ); - }); - }); - describe("when there are multiple attributes with same key", () => { - it("collects values into one array for same key", () => { - assert.deepEqual( - flattenAttributes(testAttrsDuplicatedKeys), - expectedTestAttrsFlattened, - ); + describe("standardizeKey", () => { + it("should convert strings to camel case", () => { + assert.equal(standardizeKey("hello world"), "helloWorld"); + assert.equal(standardizeKey("camels-are-cool"), "camelsAreCool"); + assert.equal(standardizeKey("cockroach"), "cockroach"); }); - }); - describe("when attribute key/value is `spans FULL SCAN`", () => { - it("sets warn to true", () => { - const testAttrs: IAttr[] = [ - { - key: "foo", - value: "bar", - }, - { - key: "spans", - value: "FULL SCAN", - }, - ]; - const expectedTestAttrs: FlatPlanNodeAttribute[] = [ - { - key: "foo", - values: ["bar"], - warn: false, - }, - { - key: "spans", - values: ["FULL SCAN"], - warn: true, - }, - ]; - - assert.deepEqual(flattenAttributes(testAttrs), expectedTestAttrs); - }); - }); - describe("when keys are unsorted", () => { - it("puts table key first, and sorts remaining keys alphabetically", () => { - const testAttrs: IAttr[] = [ - { - key: "zebra", - value: "foo", - }, - { - key: "table", - value: "foo", - }, - { - key: "cheetah", - value: "foo", - }, - { - key: "table", - value: "bar", - }, - ]; - const expectedTestAttrs: FlatPlanNodeAttribute[] = [ - { - key: "table", - values: ["foo", "bar"], - warn: false, - }, - { - key: "cheetah", - values: ["foo"], - warn: false, - }, - { - key: "zebra", - values: ["foo"], - warn: false, - }, - ]; - - assert.deepEqual(flattenAttributes(testAttrs), expectedTestAttrs); + + it("should remove '(anti)' from the key", () => { + assert.equal(standardizeKey("lookup join (anti)"), "lookupJoin"); + assert.equal(standardizeKey("(anti) hello world"), "helloWorld"); }); }); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx index be5582586045..529ac44f286a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx @@ -8,13 +8,18 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import _ from "lodash"; import React, { Fragment } from "react"; +import _ from "lodash"; import classNames from "classnames/bind"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { Fraction } from "../statementDetails"; import { Tooltip } from "@cockroachlabs/ui-components"; +import { + getAttributeTooltip, + getOperatorTooltip, + getAttributeValueTooltip, +} from "./planTooltips"; import styles from "./planView.module.scss"; -import { Fraction } from "../statementDetails"; type IAttr = cockroach.sql.ExplainTreePlanNode.IAttr; type IExplainTreePlanNode = cockroach.sql.IExplainTreePlanNode; @@ -139,6 +144,24 @@ function shouldHideNode(nodeName: string): boolean { return nodeName === "row source to plan node"; } +// standardizeKey converts strings separated by whitespace and/or +// hyphens to camel case. '(anti)' is also removed from the resulting string. +export function standardizeKey(str: string): string { + return str + .toLowerCase() + .split(/[ -]+/) + .filter(str => str !== "(anti)") + .map((str, i) => + i === 0 + ? str + : str + .charAt(0) + .toUpperCase() + .concat(str.substring(1)), + ) + .join(""); +} + /* ************************* PLAN NODES ************************* */ function NodeAttribute({ @@ -148,17 +171,40 @@ function NodeAttribute({ }): React.ReactElement { if (!attribute.values.length || !attribute.values[0].length) return null; - const attrClassName = attribute.warn ? "warn" : ""; - const values = attribute.values.length > 1 ? `[${attribute.values.join(", ")}]` : attribute.values[0]; + const tooltipContent = getAttributeTooltip(standardizeKey(attribute.key)); + const attributeValTooltip = + attribute.values.length > 1 + ? null + : getAttributeValueTooltip(standardizeKey(attribute.values[0])); + return ( -
- {attribute.key}:{" "} - {values} +
+ + + {`${attribute.key}:`} + + {" "} + + + {values} + +
); } @@ -167,33 +213,26 @@ interface PlanNodeDetailProps { node: FlatPlanNode; } -class PlanNodeDetails extends React.Component { - constructor(props: PlanNodeDetailProps) { - super(props); - } +function PlanNodeDetails({ node }: PlanNodeDetailProps): React.ReactElement { + const tooltipContent = getOperatorTooltip(standardizeKey(node.name)); - renderNodeDetails() { - const node = this.props.node; - if (node.attrs && node.attrs.length > 0) { - return ( -
+ return ( +
+ {NODE_ICON}{" "} + + + {node.name} + + + {node.attrs && node.attrs.length > 0 && ( +
{node.attrs.map((attr, idx) => ( ))}
- ); - } - } - - render() { - const node = this.props.node; - return ( -
- {NODE_ICON} {node.name} - {this.renderNodeDetails()} -
- ); - } + )} +
+ ); } type PlanNodeProps = { node: FlatPlanNode }; @@ -227,110 +266,76 @@ interface PlanViewProps { globalProperties: GlobalPropertiesType; } -interface PlanViewState { - expanded: boolean; - showExpandDirections: boolean; -} - -export class PlanView extends React.Component { - private readonly innerContainer: React.RefObject; - constructor(props: PlanViewProps) { - super(props); - this.state = { - expanded: false, - showExpandDirections: true, - }; - this.innerContainer = React.createRef(); - } - - toggleExpanded = () => { - this.setState(state => ({ - expanded: !state.expanded, - })); - }; - - showExpandDirections() { - // Only show directions to show/hide the full plan if content is longer than its max-height. - const containerObj = this.innerContainer.current; - return containerObj.scrollHeight > containerObj.clientHeight; - } - - componentDidMount() { - this.setState({ showExpandDirections: this.showExpandDirections() }); - } +export function PlanView({ + title, + plan, + globalProperties, +}: PlanViewProps): React.ReactElement { + const flattenedPlanNodeRoot = flattenTreeAttributes(plan); + + const globalAttrs: FlatPlanNodeAttribute[] = [ + { + key: "distribution", + values: [fractionToString(globalProperties.distribution)], + warn: false, // distribution is never warned + }, + { + key: "vectorized", + values: [fractionToString(globalProperties.vectorized)], + warn: false, // vectorized is never warned + }, + ]; + + const lastSampledHelpText = ( + + If the time from the last sample is greater than 5 minutes, a new plan + will be sampled. This frequency can be configured with the cluster setting{" "} + +
+          sql.metrics.statement_details.plan_collection.period
+        
+
+ . +
+ ); - render() { - const { plan, globalProperties } = this.props; - const flattenedPlanNodeRoot = flattenTreeAttributes(plan); - - const globalAttrs: FlatPlanNodeAttribute[] = [ - { - key: "distribution", - values: [fractionToString(globalProperties.distribution)], - warn: false, // distribution is never warned - }, - { - key: "vectorized", - values: [fractionToString(globalProperties.vectorized)], - warn: false, // vectorized is never warned - }, - ]; - - const lastSampledHelpText = ( - - If the time from the last sample is greater than 5 minutes, a new plan - will be sampled. This frequency can be configured with the cluster - setting{" "} - -
-            sql.metrics.statement_details.plan_collection.period
-          
-
- . -
- ); - - return ( - - - - - - - - - + + +
-

- {this.props.title} -

-
- -
-
i
-
-
-
-
-
-
-
- {globalAttrs.map((attr, idx) => ( - - ))} -
- + return ( + + + + + + + + + - - -
+

+ {title} +

+
+ +
+
i
+
+
+
+
+
+
+
+ {globalAttrs.map((attr, idx) => ( + + ))}
+
-
- ); - } +
+
+ ); } diff --git a/pkg/ui/workspaces/cluster-ui/src/util/docs.ts b/pkg/ui/workspaces/cluster-ui/src/util/docs.ts index 9728bbca1fc4..6e1950463287 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/docs.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/docs.ts @@ -88,6 +88,27 @@ export const upgradeCockroachVersion = export const enterpriseLicensing = "https://www.cockroachlabs.com/docs/stable/enterprise-licensing.html"; +// Explain plan +export const distSql = docsURL("architecture/sql-layer.html#distsql"); +export const vectorizedExecution = docsURL( + "vectorized-execution.html#configuring-vectorized-execution", +); +export const mergeJoin = docsURL("joins.html#merge-joins"); +export const lookupJoin = docsURL("joins.html#lookup-joins"); +export const hashJoin = docsURL("joins.html#hash-joins"); +export const invertedJoin = docsURL("joins.html#inverted-joins"); +export const indexJoin = docsURL("indexes.html#storing-columns"); +export const fullScan = docsURL( + "sql-tuning-with-explain.html#issue-full-table-scans", +); +export const secondaryIndex = docsURL("schema-design-indexes.html"); +export const lockingStrength = docsURL( + "explain.html#find-out-if-a-statement-is-using-select-for-update-locking", +); +export const transactionLayerOverview = docsURL( + "architecture/transaction-layer.html#overview", +); + // Not actually a docs URL. export const startTrial = "https://www.cockroachlabs.com/pricing/start-trial/"; export const transactionsTable = docsURL("ui-transactions-page.html");