From d2f48b991a6f7dd5c753738fbe14f7e30a8fb68a Mon Sep 17 00:00:00 2001 From: Mark Sirek Date: Sat, 5 Aug 2023 17:57:20 -0700 Subject: [PATCH 1/7] sql: fast path uniqueness checks for single-row insert This adds support for building and executing simple INSERT statements with a single-row VALUES clause where any required uniqueness constraint checks can be handled via a constrained scan on an index. This includes INSERT cases such as: - a single-row VALUES clause into a REGIONAL BY ROW table with a PRIMARY KEY which has a UUID column generated by default, ie. `id UUID PRIMARY KEY DEFAULT gen_random_uuid()`, where the crdb_region column is not specified in the VALUES clause; either the gateway region is used or it is computed based on other column values. - a single-row VALUES clause into a REGIONAL BY ROW table with a hash sharded unique index where the crdb_region column is not specified in the VALUES clause In optbuild, when creating a uniqueness check for rows which are added to a table, a fast path index check relation is also built when the mutation source is a single-row values expression or WithScan from a single-row values expression. That relation is a filtered Select of a Scan from the target table, where the filters equate all of the unique check columns with their corresponding constants or placeholders from the Values expression. If there is a uniqueness check with a partial index predicate, fast path is disallowed. A new exploration rule called InsertFastPath is added to walk the memo group members created during exploration in `FastPathUniqueChecks` of the `InsertExpr`, to find any which have been rewritten as a constrained `ScanExpr`. If found, that means that Scan fully represents the lookup needed to check for duplicate entries and the Scan constraint can be used to identify the constants to use in a KV lookup on the scanned index in a fast path check. Function CanUseUniqueChecksForInsertFastPath walks the expressions generated during exploration of the `FastPathUniqueChecks.Check` relation. If a constrained scan is found, it is used to build elements of the `FastPathUniqueChecksItemPrivate` structure to communicate to the execbuilder the table and index to use for the check, and the column ids in the insert row to use for building the fast path KV request. In addition, a new `DatumsFromConstraint` field is added, consisting of a ScalarListExpr of TupleExprs specifying the index key, which allows an index key column to be matched with multiple Datums. One insert row may result in more than one KV lookup for a given uniqueness constraint. These items are used to build the `InsertFastPathFKUniqCheck` structure in the execbuilder. The new `FastPathUniqueChecksItemPrivate` is built into a new the corresponding `FastPathUniqueChecksItem`s of a new `FastPathUniqueChecksExpr` and communicated to the caller via return value `newFastPathUniqueChecks`. A small adjustment is made in the coster to make the fast path unique constraint slightly cheaper, so it should always be chosen over the original non-fast path check. Epic: CRDB-26290 Fixes: #58047 Release note (performance improvement): This patch adds support for insert fast-path uniqueness checks on REGIONAL BY ROW tables where the source is a VALUES clause with a single row. This results in a reduction in latency for single-row inserts to REGIONAL BY ROW tables and hash-sharded REGIONAL BY ROW tables with unique indexes. --- ...partitioning_hash_sharded_index_query_plan | 147 +---- ...ional_by_row_hash_sharded_index_query_plan | 185 +----- .../regional_by_row_insert_fast_path | 188 ++++++ .../logic_test/regional_by_row_query_behavior | 614 ++++++++++++++++-- .../BUILD.bazel | 2 +- .../generated_test.go | 7 + pkg/sql/distsql_spec_exec_factory.go | 1 + pkg/sql/logictest/testdata/logic_test/unique | 78 +++ pkg/sql/opt/exec/execbuilder/BUILD.bazel | 1 - pkg/sql/opt/exec/execbuilder/builder.go | 2 +- pkg/sql/opt/exec/execbuilder/mutation.go | 155 +++-- pkg/sql/opt/exec/execbuilder/testdata/unique | 603 +++++++++++++++++ pkg/sql/opt/exec/explain/emit.go | 6 + pkg/sql/opt/exec/factory.go | 3 + pkg/sql/opt/exec/factory.opt | 10 +- pkg/sql/opt/memo/BUILD.bazel | 2 + pkg/sql/opt/memo/insert_fastpath.go | 90 +++ .../opt/optbuilder/mutation_builder_unique.go | 12 +- pkg/sql/opt/xform/BUILD.bazel | 1 + pkg/sql/opt/xform/coster.go | 11 + pkg/sql/opt/xform/insert_funcs.go | 179 +++++ pkg/sql/opt/xform/rules/insert.opt | 33 + pkg/sql/opt/xform/testdata/rules/insert | 232 ++++++- pkg/sql/opt_exec_factory.go | 8 + 24 files changed, 2146 insertions(+), 424 deletions(-) create mode 100644 pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_insert_fast_path create mode 100644 pkg/sql/opt/memo/insert_fastpath.go create mode 100644 pkg/sql/opt/xform/insert_funcs.go create mode 100644 pkg/sql/opt/xform/rules/insert.opt diff --git a/pkg/ccl/logictestccl/testdata/logic_test/partitioning_hash_sharded_index_query_plan b/pkg/ccl/logictestccl/testdata/logic_test/partitioning_hash_sharded_index_query_plan index 0bf3de776fb6..352f1d5a5fb3 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/partitioning_hash_sharded_index_query_plan +++ b/pkg/ccl/logictestccl/testdata/logic_test/partitioning_hash_sharded_index_query_plan @@ -338,54 +338,18 @@ EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_pk (id, part) VALUES (4321, 'seattle distribution: local vectorized: true · -• root -│ columns: () -│ -├── • insert -│ │ columns: () -│ │ estimated row count: 0 (missing stats) -│ │ into: t_unique_hash_pk(crdb_internal_id_shard_16, id, part) -│ │ -│ └── • values -│ columns: (crdb_internal_id_shard_16_comp, column1, column2, check1, check2) -│ size: 5 columns, 1 row -│ row 0, expr 0: 4321 -│ row 0, expr 1: 'seattle' -│ row 0, expr 2: 11 -│ row 0, expr 3: true -│ row 0, expr 4: true -│ -└── • constraint-check - │ - └── • error if rows - │ columns: () - │ - └── • project - │ columns: (id) - │ - └── • cross join (inner) - │ columns: (id, crdb_internal_id_shard_16, id, part) - │ estimated row count: 1 (missing stats) - │ - ├── • values - │ columns: (id) - │ size: 1 column, 1 row - │ row 0, expr 0: 4321 - │ - └── • limit - │ columns: (crdb_internal_id_shard_16, id, part) - │ count: 1 - │ - └── • filter - │ columns: (crdb_internal_id_shard_16, id, part) - │ estimated row count: 6 (missing stats) - │ filter: (crdb_internal_id_shard_16 != 11) OR (part != 'seattle') - │ - └── • scan - columns: (crdb_internal_id_shard_16, id, part) - estimated row count: 0 (missing stats) - table: t_unique_hash_pk@t_unique_hash_pk_pkey - spans: /"new york"/11/4321/0 /"seattle"/11/4321/0 +• insert fast path + columns: () + estimated row count: 0 (missing stats) + into: t_unique_hash_pk(crdb_internal_id_shard_16, id, part) + auto commit + uniqueness check: t_unique_hash_pk@t_unique_hash_pk_pkey + size: 5 columns, 1 row + row 0, expr 0: 11 + row 0, expr 1: 4321 + row 0, expr 2: 'seattle' + row 0, expr 3: true + row 0, expr 4: true query T EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_pk (id, part) VALUES (4321, 'seattle') ON CONFLICT DO NOTHING; @@ -946,79 +910,20 @@ EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_sec_key (id, email, part) VALUES (43 distribution: local vectorized: true · -• root -│ columns: () -│ -├── • insert -│ │ columns: () -│ │ estimated row count: 0 (missing stats) -│ │ into: t_unique_hash_sec_key(id, email, part, crdb_internal_email_shard_16) -│ │ -│ └── • values -│ columns: (column1, column2, column3, crdb_internal_email_shard_16_comp, check1, check2) -│ size: 6 columns, 1 row -│ row 0, expr 0: 4321 -│ row 0, expr 1: 'some_email' -│ row 0, expr 2: 'seattle' -│ row 0, expr 3: 1 -│ row 0, expr 4: true -│ row 0, expr 5: true -│ -├── • constraint-check -│ │ -│ └── • error if rows -│ │ columns: () -│ │ -│ └── • project -│ │ columns: (id) -│ │ -│ └── • cross join (inner) -│ │ columns: (id, id, part) -│ │ estimated row count: 1 (missing stats) -│ │ -│ ├── • values -│ │ columns: (id) -│ │ size: 1 column, 1 row -│ │ row 0, expr 0: 4321 -│ │ -│ └── • scan -│ columns: (id, part) -│ estimated row count: 1 (missing stats) -│ table: t_unique_hash_sec_key@t_unique_hash_sec_key_pkey -│ spans: /"new york"/4321/0 -│ limit: 1 -│ -└── • constraint-check - │ - └── • error if rows - │ columns: () - │ - └── • project - │ columns: (email) - │ - └── • cross join (inner) - │ columns: (email, id, email, part) - │ estimated row count: 1 (missing stats) - │ - ├── • values - │ columns: (email) - │ size: 1 column, 1 row - │ row 0, expr 0: 'some_email' - │ - └── • limit - │ columns: (id, email, part) - │ count: 1 - │ - └── • filter - │ columns: (id, email, part) - │ estimated row count: 6 (missing stats) - │ filter: (id != 4321) OR (part != 'seattle') - │ - └── • scan - columns: (id, email, part) - estimated row count: 0 (missing stats) - table: t_unique_hash_sec_key@idx_uniq_hash_email - spans: /"new york"/1/"some_email"/0 /"seattle"/1/"some_email"/0 +• insert fast path + columns: () + estimated row count: 0 (missing stats) + into: t_unique_hash_sec_key(id, email, part, crdb_internal_email_shard_16) + auto commit + uniqueness check: t_unique_hash_sec_key@t_unique_hash_sec_key_pkey + uniqueness check: t_unique_hash_sec_key@idx_uniq_hash_email + size: 6 columns, 1 row + row 0, expr 0: 4321 + row 0, expr 1: 'some_email' + row 0, expr 2: 'seattle' + row 0, expr 3: 1 + row 0, expr 4: true + row 0, expr 5: true query T EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_sec_key (id, email, part) VALUES (4321, 'some_email', 'seattle') ON CONFLICT DO NOTHING; diff --git a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_hash_sharded_index_query_plan b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_hash_sharded_index_query_plan index 09eef6329c31..1469e7df85db 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_hash_sharded_index_query_plan +++ b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_hash_sharded_index_query_plan @@ -359,65 +359,18 @@ EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_pk (id) VALUES (4321); distribution: local vectorized: true · -• root -│ columns: () -│ -├── • insert -│ │ columns: () -│ │ estimated row count: 0 (missing stats) -│ │ into: t_unique_hash_pk(crdb_internal_id_shard_16, id, crdb_region) -│ │ -│ └── • values -│ columns: (crdb_internal_id_shard_16_comp, column1, crdb_region_default, check1, check2) -│ size: 5 columns, 1 row -│ row 0, expr 0: 4321 -│ row 0, expr 1: 'ap-southeast-2' -│ row 0, expr 2: 11 -│ row 0, expr 3: true -│ row 0, expr 4: true -│ -└── • constraint-check - │ - └── • error if rows - │ columns: () - │ - └── • project - │ columns: (id) - │ - └── • cross join (inner) - │ columns: (id, crdb_internal_id_shard_16, id, crdb_region) - │ estimated row count: 1 (missing stats) - │ - ├── • values - │ columns: (id) - │ size: 1 column, 1 row - │ row 0, expr 0: 4321 - │ - └── • limit - │ columns: (crdb_internal_id_shard_16, id, crdb_region) - │ count: 1 - │ - └── • filter - │ columns: (crdb_internal_id_shard_16, id, crdb_region) - │ estimated row count: 6 (missing stats) - │ filter: (crdb_internal_id_shard_16 != 11) OR (crdb_region != 'ap-southeast-2') - │ - └── • union all - │ columns: (crdb_internal_id_shard_16, id, crdb_region) - │ estimated row count: 1 (missing stats) - │ limit: 3 - │ - ├── • scan - │ columns: (crdb_internal_id_shard_16, id, crdb_region) - │ estimated row count: 1 (missing stats) - │ table: t_unique_hash_pk@t_unique_hash_pk_pkey - │ spans: /"@"/11/4321/0 - │ - └── • scan - columns: (crdb_internal_id_shard_16, id, crdb_region) - estimated row count: 1 (missing stats) - table: t_unique_hash_pk@t_unique_hash_pk_pkey - spans: /"\x80"/11/4321/0 /"\xc0"/11/4321/0 +• insert fast path + columns: () + estimated row count: 0 (missing stats) + into: t_unique_hash_pk(crdb_internal_id_shard_16, id, crdb_region) + auto commit + uniqueness check: t_unique_hash_pk@t_unique_hash_pk_pkey + size: 5 columns, 1 row + row 0, expr 0: 11 + row 0, expr 1: 4321 + row 0, expr 2: 'ap-southeast-2' + row 0, expr 3: true + row 0, expr 4: true query T EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_pk (id) VALUES (4321) ON CONFLICT DO NOTHING; @@ -1024,106 +977,20 @@ EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_sec_key (id, email) VALUES (4321, 's distribution: local vectorized: true · -• root -│ columns: () -│ -├── • insert -│ │ columns: () -│ │ estimated row count: 0 (missing stats) -│ │ into: t_unique_hash_sec_key(id, email, crdb_region, crdb_internal_email_shard_16) -│ │ -│ └── • values -│ columns: (column1, column2, crdb_region_default, crdb_internal_email_shard_16_comp, check1, check2) -│ size: 6 columns, 1 row -│ row 0, expr 0: 4321 -│ row 0, expr 1: 'some_email' -│ row 0, expr 2: 'ap-southeast-2' -│ row 0, expr 3: 1 -│ row 0, expr 4: true -│ row 0, expr 5: true -│ -├── • constraint-check -│ │ -│ └── • error if rows -│ │ columns: () -│ │ -│ └── • project -│ │ columns: (id) -│ │ -│ └── • cross join (inner) -│ │ columns: (id, id, crdb_region) -│ │ estimated row count: 1 (missing stats) -│ │ -│ ├── • values -│ │ columns: (id) -│ │ size: 1 column, 1 row -│ │ row 0, expr 0: 4321 -│ │ -│ └── • scan -│ columns: (id, crdb_region) -│ estimated row count: 1 (missing stats) -│ table: t_unique_hash_sec_key@t_unique_hash_sec_key_pkey -│ spans: /"\x80"/4321/0 /"\xc0"/4321/0 -│ limit: 1 -│ -└── • constraint-check - │ - └── • error if rows - │ columns: () - │ - └── • project - │ columns: (email) - │ - └── • cross join (inner) - │ columns: (email, id, email, crdb_region) - │ estimated row count: 1 (missing stats) - │ - ├── • values - │ columns: (email) - │ size: 1 column, 1 row - │ row 0, expr 0: 'some_email' - │ - └── • limit - │ columns: (id, email, crdb_region) - │ count: 1 - │ - └── • distinct - │ columns: (id, email, crdb_region) - │ estimated row count: 6 (missing stats) - │ distinct on: id, crdb_region - │ - └── • union all - │ columns: (id, email, crdb_region) - │ estimated row count: 2 (missing stats) - │ - ├── • filter - │ │ columns: (id, email, crdb_region) - │ │ estimated row count: 1 (missing stats) - │ │ filter: id != 4321 - │ │ - │ └── • union all - │ │ columns: (id, email, crdb_region) - │ │ estimated row count: 1 (missing stats) - │ │ limit: 1 - │ │ - │ ├── • scan - │ │ columns: (id, email, crdb_region) - │ │ estimated row count: 1 (missing stats) - │ │ table: t_unique_hash_sec_key@idx_uniq_hash_email - │ │ spans: /"@"/1/"some_email"/0 - │ │ - │ └── • scan - │ columns: (id, email, crdb_region) - │ estimated row count: 1 (missing stats) - │ table: t_unique_hash_sec_key@idx_uniq_hash_email - │ spans: /"\x80"/1/"some_email"/0 /"\xc0"/1/"some_email"/0 - │ parallel - │ - └── • scan - columns: (id, email, crdb_region) - estimated row count: 1 (missing stats) - table: t_unique_hash_sec_key@idx_uniq_hash_email - spans: /"\x80"/1/"some_email"/0 /"\xc0"/1/"some_email"/0 +• insert fast path + columns: () + estimated row count: 0 (missing stats) + into: t_unique_hash_sec_key(id, email, crdb_region, crdb_internal_email_shard_16) + auto commit + uniqueness check: t_unique_hash_sec_key@t_unique_hash_sec_key_pkey + uniqueness check: t_unique_hash_sec_key@idx_uniq_hash_email + size: 6 columns, 1 row + row 0, expr 0: 4321 + row 0, expr 1: 'some_email' + row 0, expr 2: 'ap-southeast-2' + row 0, expr 3: 1 + row 0, expr 4: true + row 0, expr 5: true query T EXPLAIN (VERBOSE) INSERT INTO t_unique_hash_sec_key (id, email) VALUES (4321, 'some_email') ON CONFLICT DO NOTHING; diff --git a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_insert_fast_path b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_insert_fast_path new file mode 100644 index 000000000000..a419cf578e7c --- /dev/null +++ b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_insert_fast_path @@ -0,0 +1,188 @@ +# LogicTest: multiregion-9node-3region-3azs !metamorphic-batch-sizes +# This file contains a subset of insert fast path tests on REGIONAL BY +# ROW tables, The tests are split off from the EXPLAIN query runs in order to +# use randomized column families to achieve more test coverage. + +# Set the closed timestamp interval to be short to shorten the amount of time +# we need to wait for the system config to propagate. +statement ok +SET CLUSTER SETTING kv.closed_timestamp.side_transport_interval = '10ms'; + +statement ok +SET CLUSTER SETTING kv.closed_timestamp.target_duration = '10ms'; + +statement ok +SET CLUSTER SETTING kv.rangefeed.closed_timestamp_refresh_interval = '10ms'; + +statement ok +CREATE DATABASE multi_region_test_db PRIMARY REGION "ca-central-1" REGIONS "ap-southeast-2", "us-east-1" SURVIVE REGION FAILURE; + +statement ok +USE multi_region_test_db + +subtest insertFastPathUnique + +statement ok +SET database = multi_region_test_db + +statement ok +CREATE TABLE users2 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name STRING NOT NULL, + email STRING NOT NULL UNIQUE, + INDEX (name) +) LOCALITY REGIONAL BY ROW + +statement ok +INSERT INTO users2 (crdb_region, name, email) +VALUES ('us-east-1', 'Craig Roacher', 'craig@cockroachlabs.com') + +# Verify insert fast path detects unique constraint violations. +statement error pq: duplicate key value violates unique constraint "users2_email_key"\nDETAIL: Key \(email\)=\('craig@cockroachlabs.com'\) already exists\. +INSERT INTO users2 (name, email) +VALUES ('Craig Roacher', 'craig@cockroachlabs.com') + +# Verify insert fast path detects unique constraint violations. +statement error pq: duplicate key value violates unique constraint "users2_email_key"\nDETAIL: Key \(email\)=\('craig@cockroachlabs.com'\) already exists\. +INSERT INTO users2 (crdb_region, name, email) +VALUES ('ap-southeast-2', 'Craig Roacher', 'craig@cockroachlabs.com') + +# Verify insert fast path detects unique constraint violations. +statement error pq: duplicate key value violates unique constraint "users2_email_key"\nDETAIL: Key \(email\)=\('craig@cockroachlabs.com'\) already exists\. +INSERT INTO users2 (crdb_region, name, email) +VALUES ('ca-central-1', 'Craig Roacher', 'craig@cockroachlabs.com') + +statement ok +INSERT INTO users2 (name, email) +VALUES ('Bill Roacher', 'bill@cockroachlabs.com'), ('Jill Roacher', 'jill@cockroachlabs.com') + +# Verify multi-row insert fast path detects unique constraint violations. +statement error pq: duplicate key value violates unique constraint "users2_email_key"\nDETAIL: Key \(email\)=\('bill@cockroachlabs.com'\) already exists\. +INSERT INTO users2 (name, email) +VALUES ('Bill Roacher', 'bill@cockroachlabs.com'), ('Jill Roacher', 'jill@cockroachlabs.com') + +# Multi-row insert fast path uniqueness checks as a prepared statement not +# currently supported. +statement ok +PREPARE e1 AS EXPLAIN INSERT INTO users2 (name, email) +VALUES ($1, $2), ($3, $4) + +statement ok +PREPARE p1 AS INSERT INTO users2 (name, email) +VALUES ($1, $2), ($3, $4) + +# Verify parameterized multi-row insert fast path detects unique constraint +# violations. +statement error pq: duplicate key value violates unique constraint "users2_email_key"\nDETAIL: Key \(email\)=\('bill@cockroachlabs.com'\) already exists\. +EXECUTE p1 ('Bill Roacher', 'bill@cockroachlabs.com', 'Jo Roacher', 'jo@cockroachlabs.com') + +statement ok +CREATE TABLE users4 ( + id UUID, + name STRING NOT NULL PRIMARY KEY, + email STRING NOT NULL UNIQUE +) LOCALITY REGIONAL BY ROW + +statement ok +INSERT INTO users4 SELECT * FROM users2 + +statement ok +ALTER TABLE users2 ADD CONSTRAINT users4_fk FOREIGN KEY (name) REFERENCES users4 (name) ON DELETE CASCADE + +# Verify multi-row insert fast path detects unique constraint violations. +statement error pq: duplicate key value violates unique constraint "users2_email_key"\nDETAIL: Key \(email\)=\('bill@cockroachlabs.com'\) already exists\. +INSERT INTO users2 (name, email) +VALUES ('Bill Roacher', 'bill@cockroachlabs.com'), ('Jo Roacher', 'jo@cockroachlabs.com') + +statement ok +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id2 INT, + username STRING NOT NULL, + UNIQUE INDEX id2_idx(id2) +) LOCALITY REGIONAL BY ROW; + +statement ok +INSERT INTO users (crdb_region, id, id2, username) +VALUES ('us-east-1', '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 2, 'Craig Roacher') + +statement ok +INSERT INTO users (crdb_region, id, id2, username) +VALUES ('ap-southeast-2', '5ebfedee-0dcf-41e6-a315-5fa0b51b9889', 19, 'Dale Roacher') + +statement ok +CREATE TABLE user_settings2 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id2 INT, + user_id UUID NOT NULL, + value STRING NOT NULL, + INDEX(user_id), + UNIQUE INDEX id2_idx(id2), + FOREIGN KEY (user_id, crdb_region) REFERENCES users (id, crdb_region) +) LOCALITY REGIONAL BY ROW; + +statement ok +CREATE TABLE user_settings3 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id2 INT, + user_id UUID NOT NULL, + value STRING NOT NULL, + INDEX(user_id), + UNIQUE INDEX id2_idx(id2), + FOREIGN KEY (user_id) REFERENCES users (id) +) LOCALITY REGIONAL BY ROW; + +# Insert fast path should catch this uniqueness violation within the batch of +# input rows. +statement error pq: duplicate key value violates unique constraint "id2_idx"\nDETAIL: Key \(id2\)=\(2\) already exists\. +INSERT INTO user_settings2 (id, id2, user_id, value) +VALUES ('5ebfedee-0dcf-41e6-a315-5fa0b51b9883', 2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9889', 'foo'), + ('5ebfedee-0dcf-41e6-a315-5fa0b51b9884', 2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9889', 'foo') + +# The first insert doesn't violate uniqueness. +statement ok +INSERT INTO user_settings2 (crdb_region, id2, user_id, value) VALUES ('us-east-1', 2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 'foo') + +# Verify insert fast path errors out uniqueness violations correctly. +statement error pq: duplicate key value violates unique constraint "id2_idx"\nDETAIL: Key \(id2\)=\(2\) already exists\. +INSERT INTO user_settings2 (crdb_region, id2, user_id, value) VALUES ('us-east-1', 2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 'foo') + +# Verify insert fast path errors out FK violations correctly when there's also +# a UNIQUE WITHOUT INDEX constraint. +statement error pq: insert on table "user_settings2" violates foreign key constraint "user_settings2_user_id_crdb_region_fkey"\nDETAIL: Key \(user_id, crdb_region\)=\('5ebfedee-0dcf-41e6-a315-5fa0b51b9889', 'us-east-1'\) is not present in table "users"\. +INSERT INTO user_settings2 (crdb_region, id2, user_id, value) VALUES ('us-east-1', 9, '5ebfedee-0dcf-41e6-a315-5fa0b51b9889', 'foo') + +# Verify insert fast path with both a uniqueness violation and an FK violation +# reports the uniqueness violation (the same as is done for non-fast path). +statement error pq: duplicate key value violates unique constraint "id2_idx"\nDETAIL: Key \(id2\)=\(2\) already exists\. +INSERT INTO user_settings2 (crdb_region, id2, user_id, value) VALUES ('us-east-1', 2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9889', 'foo') + +# Hash-sharded RBR table. +statement ok +CREATE TABLE hash_sharded_rbr_computed ( + region_id STRING(10) NOT NULL, + my_uuid UUID NOT NULL, + my_uuid2 UUID NOT NULL, + another_id INT NOT NULL, + row_ts TIMESTAMP NULL, + crdb_region_col crdb_internal_region NOT VISIBLE NOT NULL AS (CASE WHEN substring(region_id, 1:::INT8, 4:::INT8) = 'east':::STRING THEN 'us-east-1':::crdb_internal_region WHEN substring(region_id, 1:::INT8, 2:::INT8) = 'ap':::STRING THEN 'ap-southeast-2':::crdb_internal_region ELSE 'ca-central-1':::crdb_internal_region END) STORED, + CONSTRAINT "primary" PRIMARY KEY (region_id ASC) USING HASH +) LOCALITY REGIONAL BY ROW AS crdb_region_col; + +statement ok +CREATE UNIQUE INDEX idx_date ON hash_sharded_rbr_computed (row_ts ASC, another_id ASC) USING HASH + +statement ok +INSERT +INTO + hash_sharded_rbr_computed (region_id, my_uuid, my_uuid2, another_id, row_ts) +VALUES + ('east1234', gen_random_uuid(), gen_random_uuid(), 1, TIMESTAMP '2016-01-25 10:10:10.555555') + +# Test fast path unique checks work properly on a hash-sharded RBR table. +statement error pq: duplicate key value violates unique constraint "idx_date"\nDETAIL: Key \(row_ts, another_id\)=\('2016-01-25 10:10:10.555555', 1\) already exists\. +INSERT +INTO + hash_sharded_rbr_computed (region_id, my_uuid, my_uuid2, another_id, row_ts) +VALUES + ('ap1234', gen_random_uuid(), gen_random_uuid(), 1, TIMESTAMP '2016-01-25 10:10:10.555555') diff --git a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_query_behavior b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_query_behavior index e60c7a0f492c..1e4a5f992d05 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_query_behavior +++ b/pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_query_behavior @@ -1526,8 +1526,7 @@ LIMIT 1] OFFSET 2 statement ok ALTER TABLE regional_by_row_table ADD CONSTRAINT unique_b_a UNIQUE(b, a) -# We should plan uniqueness checks for all unique indexes in -# REGIONAL BY ROW tables. +# The unique partial index disallows fast path insert. query T retry SELECT * FROM [EXPLAIN INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES (1, 1, 1, 1)] OFFSET 2 ---- @@ -1900,41 +1899,11 @@ query T retry SELECT * FROM [EXPLAIN INSERT INTO regional_by_row_table_as (pk, a, b) VALUES (1, 1, 1)] OFFSET 2 ---- · -• root -│ -├── • insert -│ │ into: regional_by_row_table_as(pk, a, b, crdb_region_col) -│ │ -│ └── • values -│ size: 5 columns, 1 row -│ -└── • constraint-check - │ - └── • error if rows - │ - └── • cross join - │ - ├── • values - │ size: 1 column, 1 row - │ - └── • limit - │ count: 1 - │ - └── • filter - │ filter: (pk != 1) OR (crdb_region_col != 'us-east-1') - │ - └── • union all - │ limit: 3 - │ - ├── • scan - │ missing stats - │ table: regional_by_row_table_as@regional_by_row_table_as_b_key - │ spans: [/'ap-southeast-2'/1 - /'ap-southeast-2'/1] - │ - └── • scan - missing stats - table: regional_by_row_table_as@regional_by_row_table_as_b_key - spans: [/'ca-central-1'/1 - /'ca-central-1'/1] [/'us-east-1'/1 - /'us-east-1'/1] +• insert fast path + into: regional_by_row_table_as(pk, a, b, crdb_region_col) + auto commit + uniqueness check: regional_by_row_table_as@regional_by_row_table_as_b_key + size: 5 columns, 1 row statement error pq: duplicate key value violates unique constraint "regional_by_row_table_as_pkey"\nDETAIL: Key \(pk\)=\(1\) already exists\. INSERT INTO regional_by_row_table_as (pk, a, b) VALUES (1, 1, 1) @@ -2947,29 +2916,11 @@ VALUES ('Craig Roacher', 'craig@cockroachlabs.com') distribution: local vectorized: true · -• root -│ -├── • insert -│ │ into: users(id, name, email, crdb_region) -│ │ -│ └── • buffer -│ │ label: buffer 1 -│ │ -│ └── • values -│ size: 5 columns, 1 row -│ -└── • constraint-check - │ - └── • error if rows - │ - └── • lookup join (semi) - │ table: users@users_email_key - │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column2 = email) - │ pred: (id_default != id) OR (crdb_region_default != crdb_region) - │ - └── • scan buffer - estimated row count: 1 - label: buffer 1 +• insert fast path + into: users(id, name, email, crdb_region) + auto commit + uniqueness check: users@users_email_key + size: 5 columns, 1 row statement ok SET index_recommendations_enabled = false; @@ -2985,7 +2936,8 @@ CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id2 INT, username STRING NOT NULL, - UNIQUE INDEX id2_idx(id2) + UNIQUE INDEX id2_idx(id2), + FAMILY (id, id2, username) ) LOCALITY REGIONAL BY ROW; statement ok @@ -3758,3 +3710,543 @@ project └── filters ├── a.created_at:3 = b.created_at:18 [outer=(3,18), constraints=(/3: (/NULL - ]; /18: (/NULL - ]), fd=(3)==(18), (18)==(3)] └── a.updated_at:4 = b.updated_at:19 [outer=(4,19), constraints=(/4: (/NULL - ]; /19: (/NULL - ]), fd=(4)==(19), (19)==(4)] + +subtest insertFastPathUnique + +statement ok +SET database = multi_region_test_db + +statement ok +CREATE TABLE users2 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name STRING NOT NULL, + email STRING NOT NULL UNIQUE, + INDEX (name), + FAMILY (id, name, email) +) LOCALITY REGIONAL BY ROW + + +# Single-row insert fast path which needs no special handling. +query T +EXPLAIN INSERT INTO users2 (crdb_region, name, email) +VALUES ('ap-southeast-2', 'Craig Roacher', 'craig@cockroachlabs.com') +---- +distribution: local +vectorized: true +· +• insert fast path + into: users2(id, name, email, crdb_region) + auto commit + uniqueness check: users2@users2_email_key + size: 5 columns, 1 row + +# Multi-row insert fast path uniqueness checks not currently supported. +query T +EXPLAIN INSERT INTO users2 (name, email) +VALUES ('Bill Roacher', 'bill@cockroachlabs.com'), ('Jill Roacher', 'jill@cockroachlabs.com') +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: users2(id, name, email, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 2 columns, 2 rows +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: users2@users2_email_key + │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column2 = email) + │ pred: (id_default != id) OR (crdb_region_default != crdb_region) + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +# Multi-row insert fast path uniqueness checks as a prepared statement not +# currently supported. +statement ok +PREPARE e1 AS EXPLAIN INSERT INTO users2 (name, email) +VALUES ($1, $2), ($3, $4) + +query T nosort +EXECUTE e1 ('Bill Roacher', 'bill@cockroachlabs.com', 'Jo Roacher', 'jo@cockroachlabs.com') +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: users2(id, name, email, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 2 columns, 2 rows +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: users2@users2_email_key + │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column2 = email) + │ pred: (id_default != id) OR (crdb_region_default != crdb_region) + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +statement ok +SET experimental_enable_unique_without_index_constraints = true + +statement ok +CREATE TABLE users3 ( + id UUID DEFAULT gen_random_uuid(), + name STRING NOT NULL, + email STRING NOT NULL, + UNIQUE WITHOUT INDEX (email) WHERE email != name, + INDEX (name), + PRIMARY KEY (email, id), + FAMILY (id, name, email) +) LOCALITY REGIONAL BY ROW + +statement ok +RESET experimental_enable_unique_without_index_constraints + +# Verify a partial UNIQUE WITHOUT INDEX with a predicate similar to the PK +# values check created in `buildInsertionCheck` to prevent rows from matching +# themselves doesn't mistakenly allow insert fast path. +query T +EXPLAIN INSERT INTO users3 (crdb_region, name, email) +VALUES ('ap-southeast-2', 'craig@cockroachlabs.com', 'craig@cockroachlabs.com') +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: users3(id, name, email, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 5 columns, 1 row +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: users3@users3_pkey + │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column3 = email) + │ pred: ((id_default != id) OR (column1 != crdb_region)) AND (email != name) + │ + └── • filter + │ estimated row count: 0 + │ filter: column3 != column2 + │ + └── • scan buffer + estimated row count: 1 + label: buffer 1 + +query T +EXPLAIN INSERT INTO multi_region_test_db.regional_by_row_table (pk, pk2, a, b) VALUES +(1, 1, 1, -5) +---- +distribution: local +vectorized: true +· +• insert fast path + into: regional_by_row_table(pk, pk2, a, b, j, crdb_region) + auto commit + uniqueness check: regional_by_row_table@regional_by_row_table_pkey + uniqueness check: regional_by_row_table@regional_by_row_table_b_key + size: 7 columns, 1 row + +statement ok +INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES + (1, 1, 1, -5) + +statement error pq: duplicate key value violates unique constraint "regional_by_row_table_b_key"\nDETAIL: Key \(b\)=\(-5\) already exists\. +INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES + (16, 16, 15, -5) + +statement ok +PREPARE s1 AS EXPLAIN INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES ($1, $2, $3, $4); + +query T nosort +EXECUTE s1 (1, 1, 1, -5) +---- +distribution: local +vectorized: true +· +• insert fast path + into: regional_by_row_table(pk, pk2, a, b, j, crdb_region) + auto commit + uniqueness check: regional_by_row_table@regional_by_row_table_pkey + uniqueness check: regional_by_row_table@regional_by_row_table_b_key + size: 7 columns, 1 row + +statement ok +PREPARE s2 AS INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES ($1, $2, $3, $4); + +statement error pq: duplicate key value violates unique constraint "regional_by_row_table_b_key"\nDETAIL: Key \(b\)=\(-5\) already exists\. +EXECUTE s2 (6, 7, 8, -5) + +statement ok +PREPARE s3 AS EXPLAIN INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES ($1, $2, $3, $4), ($5, $6, $7, $8); + +# Multi-row insert fast path uniqueness checks not yet supported. +query T nosort +EXECUTE s3 (7, 7, 7, 7, 6, 7, 8, -5) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: regional_by_row_table(pk, pk2, a, b, j, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 4 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: regional_by_row_table@regional_by_row_table_pkey +│ │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column1 = pk) +│ │ pred: crdb_region_default != crdb_region +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: regional_by_row_table@regional_by_row_table_b_key + │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column4 = b) + │ pred: (column1 != pk) OR (crdb_region_default != crdb_region) + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +statement ok +PREPARE s4 AS INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES ($1, $2, $3, $4), ($5, $6, $7, $8); + +statement error pq: duplicate key value violates unique constraint "regional_by_row_table_b_key"\nDETAIL: Key \(b\)=\(-5\) already exists\. +EXECUTE s4 (7, 7, 7, 7, 6, 7, 8, -5) + +statement ok +CREATE TABLE user_settings2 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id2 INT, + user_id UUID NOT NULL, + value STRING NOT NULL, + INDEX(user_id), + UNIQUE INDEX id2_idx(id2), + FOREIGN KEY (user_id, crdb_region) REFERENCES users (id, crdb_region), + FAMILY (id, id2, user_id, value) +) LOCALITY REGIONAL BY ROW; + +statement ok +CREATE TABLE user_settings3 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id2 INT, + user_id UUID NOT NULL, + value STRING NOT NULL, + INDEX(user_id), + UNIQUE INDEX id2_idx(id2), + FOREIGN KEY (user_id) REFERENCES users (id), + FAMILY (id, id2, user_id, value) +) LOCALITY REGIONAL BY ROW; + +# An insert with FK constraint requiring a WithScan should still allow +# insert fast path. +query T +EXPLAIN INSERT INTO user_settings2 (id2, user_id, value) VALUES (2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 'foo') +---- +distribution: local +vectorized: true +· +• insert fast path + into: user_settings2(id, id2, user_id, value, crdb_region) + auto commit + FK check: users@users_pkey + uniqueness check: user_settings2@id2_idx + size: 6 columns, 1 row + +# An insert with FK constraint and uniqueness check inserting multiple rows +# is not supported by insert fast path. +query T +EXPLAIN +INSERT INTO user_settings2 (id2, user_id, value) +VALUES (2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 'foo'), + (2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 'foo'); +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: user_settings2(id, id2, user_id, value, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 3 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: user_settings2@id2_idx +│ │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column1 = id2) +│ │ pred: (id_default != id) OR (crdb_region_default != crdb_region) +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (anti) + │ table: users@users_pkey + │ equality: (crdb_region_default, column2) = (crdb_region,id) + │ equality cols are key + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +# Hash-sharded RBR table. +statement ok +CREATE TABLE hash_sharded_rbr_computed ( + region_id STRING(10) NOT NULL, + my_uuid UUID NOT NULL, + my_uuid2 UUID NOT NULL, + another_id INT NOT NULL, + row_ts TIMESTAMP NULL, + crdb_region_col crdb_internal_region NOT VISIBLE NOT NULL AS (CASE WHEN substring(region_id, 1:::INT8, 4:::INT8) = 'east':::STRING THEN 'us-east-1':::crdb_internal_region WHEN substring(region_id, 1:::INT8, 2:::INT8) = 'ap':::STRING THEN 'ap-southeast-2':::crdb_internal_region ELSE 'ca-central-1':::crdb_internal_region END) STORED, + CONSTRAINT "primary" PRIMARY KEY (region_id ASC) USING HASH, + FAMILY (region_id, my_uuid, my_uuid2, another_id, row_ts, crdb_region_col) +) LOCALITY REGIONAL BY ROW AS crdb_region_col; + +statement ok +CREATE UNIQUE INDEX idx_date ON hash_sharded_rbr_computed (row_ts ASC, another_id ASC) USING HASH + +# Hash-sharded RBR table with unique hash-sharded index supports fast path. +query T retry +EXPLAIN INSERT +INTO + hash_sharded_rbr_computed (region_id, my_uuid, my_uuid2, another_id, row_ts) +VALUES + ('east1234', gen_random_uuid(), gen_random_uuid(), 1, TIMESTAMP '2016-01-25 10:10:10.555555') +---- +distribution: local +vectorized: true +· +• insert fast path + into: hash_sharded_rbr_computed(region_id, my_uuid, my_uuid2, another_id, row_ts, crdb_region_col, crdb_internal_region_id_shard_16, crdb_internal_another_id_row_ts_shard_16) + auto commit + uniqueness check: hash_sharded_rbr_computed@idx_date + size: 11 columns, 1 row + +# With an additional check constraint on the crdb_region column, insert fast +# path is still picked. +statement ok +CREATE TABLE hash_sharded_rbr_computed_check ( +region_based_id STRING(10) NOT NULL, +my_uuid UUID NOT NULL, +my_uuid2 UUID NOT NULL, +another_id INT NOT NULL, +row_ts TIMESTAMP NULL, +geo_zone STRING NOT NULL, +crdb_region_col crdb_internal_region NOT VISIBLE NOT NULL AS (CASE WHEN substring(geo_zone, 1:::INT8, 4:::INT8) = 'east':::STRING THEN 'us-east-1':::crdb_internal_region WHEN substring(region_based_id, 1:::INT8, 2:::INT8) = 'ap':::STRING THEN 'ap-southeast-2':::crdb_internal_region ELSE 'ca-central-1':::crdb_internal_region END) STORED, +CONSTRAINT "primary" PRIMARY KEY (region_based_id ASC) USING HASH WITH (bucket_count=16), +CONSTRAINT c1 CHECK (crdb_region_col IN ('us-east-1':::crdb_internal_region, 'ap-southeast-2':::crdb_internal_region)), +FAMILY (region_based_id, my_uuid, my_uuid2, another_id, row_ts, geo_zone, crdb_region_col) +) LOCALITY REGIONAL BY ROW AS crdb_region_col; + +statement ok +CREATE UNIQUE INDEX idx_date ON hash_sharded_rbr_computed_check (row_ts ASC, another_id ASC) USING HASH WITH (bucket_count=16) + +# Hash-sharded RBR table with check constraint and unique hash-sharded index +# supports fast path. +query T +EXPLAIN INSERT +INTO + hash_sharded_rbr_computed_check (region_based_id, geo_zone, my_uuid, my_uuid2, another_id, row_ts) +VALUES + ('east1234', 'east1234', gen_random_uuid(), gen_random_uuid(), 1, TIMESTAMP '2016-01-25 10:10:10.555555') +---- +distribution: local +vectorized: true +· +• insert fast path + into: hash_sharded_rbr_computed_check(region_based_id, my_uuid, my_uuid2, another_id, row_ts, geo_zone, crdb_region_col, crdb_internal_region_based_id_shard_16, crdb_internal_another_id_row_ts_shard_16) + auto commit + uniqueness check: hash_sharded_rbr_computed_check@primary + uniqueness check: hash_sharded_rbr_computed_check@idx_date + size: 13 columns, 1 row + +# Hash-sharded RBR table with a unique hash-sharded index does not currently +# support multi-row insert fast path. +query T +EXPLAIN INSERT +INTO + hash_sharded_rbr_computed_check (region_based_id, geo_zone, my_uuid, my_uuid2, another_id, row_ts) +VALUES + ('east1234', 'east1234', gen_random_uuid(), gen_random_uuid(), 1, TIMESTAMP '2016-01-25 10:10:10.555555'), + ('east1235', 'east1234', gen_random_uuid(), gen_random_uuid(), 1, TIMESTAMP '2016-01-25 10:10:10.555555') +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: hash_sharded_rbr_computed_check(region_based_id, my_uuid, my_uuid2, another_id, row_ts, geo_zone, crdb_region_col, crdb_internal_region_based_id_shard_16, crdb_internal_another_id_row_ts_shard_16) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 6 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: hash_sharded_rbr_computed_check@primary +│ │ lookup condition: ((crdb_region_col IN ('ap-southeast-2', 'us-east-1')) AND (crdb_internal_region_based_id_shard_16_eq = crdb_internal_region_based_id_shard_16)) AND (region_based_id = region_based_id) +│ │ pred: (crdb_region_col != crdb_region_col) OR (crdb_internal_region_based_id_shard_16 != crdb_internal_region_based_id_shard_16) +│ │ +│ └── • render +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: hash_sharded_rbr_computed_check@idx_date + │ lookup condition: (((crdb_region_col IN ('ap-southeast-2', 'us-east-1')) AND (crdb_internal_another_id_row_ts_shard_16_eq = crdb_internal_another_id_row_ts_shard_16)) AND (row_ts = row_ts)) AND (another_id = another_id) + │ pred: ((region_based_id != region_based_id) OR (crdb_region_col != crdb_region_col)) OR (crdb_internal_region_based_id_shard_16 != crdb_internal_region_based_id_shard_16) + │ + └── • render + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +# This case doesn't currently use insert fast path, but could. +# TODO(msirek): Support insert fast path for this case. +query T +EXPLAIN INSERT INTO user_settings3 (id2, user_id, value) VALUES (2, '5ebfedee-0dcf-41e6-a315-5fa0b51b9882', 'foo') +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: user_settings3(id, id2, user_id, value, crdb_region) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 6 columns, 1 row +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: user_settings3@id2_idx +│ │ lookup condition: (crdb_region IN ('ap-southeast-2', 'ca-central-1', 'us-east-1')) AND (column1 = id2) +│ │ pred: (id_default != id) OR (crdb_region_default != crdb_region) +│ │ +│ └── • scan buffer +│ estimated row count: 1 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (anti) + │ table: users@users_pkey + │ equality cols are key + │ lookup condition: (crdb_region IN ('ca-central-1', 'us-east-1')) AND (column2 = id) + │ + └── • lookup join (anti) + │ table: users@users_pkey + │ equality cols are key + │ lookup condition: (crdb_region = 'ap-southeast-2') AND (column2 = id) + │ + └── • scan buffer + estimated row count: 1 + label: buffer 1 + +statement ok +CREATE UNIQUE INDEX ON regional_by_row_table (b, a) USING HASH + +# After adding a hash sharded unique index, fast path is still legal. +query T +SELECT * FROM [EXPLAIN INSERT INTO regional_by_row_table (pk, pk2, a, b) VALUES (1, 1, 1, 1)] OFFSET 2 +---- +· +• insert fast path + into: regional_by_row_table(pk, pk2, a, b, j, crdb_region, crdb_internal_a_b_shard_16) + auto commit + uniqueness check: regional_by_row_table@regional_by_row_table_pkey + uniqueness check: regional_by_row_table@regional_by_row_table_b_key + uniqueness check: regional_by_row_table@regional_by_row_table_b_a_key + size: 9 columns, 1 row + diff --git a/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/BUILD.bazel b/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/BUILD.bazel index a6c161e8fba5..385af70916f6 100644 --- a/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/BUILD.bazel @@ -13,7 +13,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"Pool": "large"}, - shard_count = 27, + shard_count = 28, tags = [ "ccl_test", "cpu:4", diff --git a/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/generated_test.go b/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/generated_test.go index 0d5ace49e455..2a28c2c150f7 100644 --- a/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/generated_test.go +++ b/pkg/ccl/logictestccl/tests/multiregion-9node-3region-3azs/generated_test.go @@ -232,6 +232,13 @@ func TestCCLLogic_regional_by_row_hash_sharded_index_query_plan( runCCLLogicTest(t, "regional_by_row_hash_sharded_index_query_plan") } +func TestCCLLogic_regional_by_row_insert_fast_path( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "regional_by_row_insert_fast_path") +} + func TestCCLLogic_regional_by_row_placement_restricted( t *testing.T, ) { diff --git a/pkg/sql/distsql_spec_exec_factory.go b/pkg/sql/distsql_spec_exec_factory.go index 950bb32b00e8..3d5ac70ba21d 100644 --- a/pkg/sql/distsql_spec_exec_factory.go +++ b/pkg/sql/distsql_spec_exec_factory.go @@ -977,6 +977,7 @@ func (e *distSQLSpecExecFactory) ConstructInsertFastPath( returnCols exec.TableColumnOrdinalSet, checkCols exec.CheckOrdinalSet, fkChecks []exec.InsertFastPathCheck, + uniqChecks []exec.InsertFastPathCheck, autoCommit bool, ) (exec.Node, error) { return nil, unimplemented.NewWithIssue(47473, "experimental opt-driven distsql planning: insert fast path") diff --git a/pkg/sql/logictest/testdata/logic_test/unique b/pkg/sql/logictest/testdata/logic_test/unique index 22552391a072..4c9e9e293326 100644 --- a/pkg/sql/logictest/testdata/logic_test/unique +++ b/pkg/sql/logictest/testdata/logic_test/unique @@ -948,3 +948,81 @@ NULL NULL NULL NULL + +subtest multirowValuesInsertFastPath + +statement ok +CREATE TABLE t1 ( + pk int PRIMARY KEY, + pk2 int, + a int, + b int, + j JSON, + INDEX (a), + UNIQUE WITHOUT INDEX(b, a), + INDEX (b,a) STORING(pk2, j), + INVERTED INDEX (j), + FAMILY (pk, pk2, a, b) +) + +statement ok +INSERT INTO t1 (pk, pk2, a, b, j) VALUES + (0, 0, 1, 3, '{"a": "b"}'); + +# This uses insert fast-path. Verify it catches the duplicate value of (b,a). +statement error pq: duplicate key value violates unique constraint "unique_b_a"\nDETAIL: Key \(a, b\)=\(1, 3\) already exists\. +INSERT INTO t1 (pk, pk2, a, b, j) VALUES + (2, 2, 1, 3, '{"a": "b"}'), (4, 5, 6, 7, '{"a": "b"}'); + +statement ok +CREATE TABLE multiple_uniq ( + a INT, + b INT, + c INT, + d INT, + UNIQUE WITHOUT INDEX (b, c), + UNIQUE WITHOUT INDEX (a, b, d), + UNIQUE WITHOUT INDEX (a), + INDEX (a, b, d), + INDEX (b, c), + FAMILY (a), + FAMILY (b), + FAMILY (c), + FAMILY (d) +) + +# This uses insert fast-path and should succeed. +statement ok +INSERT INTO multiple_uniq +VALUES (1,2,3,4), (5,6,7,8) + +# A table with multiple unique without index checks is eligible for insert fast +# path. Verify it catches the duplicate value of (a). +statement error pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +INSERT INTO multiple_uniq +VALUES (1,12,13,14), (15,16,17,18) + +# A table with multiple unique without index checks is eligible for insert fast +# path. Verify it catches the duplicate value of (b,c). +statement error pq: duplicate key value violates unique constraint "unique_b_c"\nDETAIL: Key \(b, c\)=\(2, 3\) already exists\. +INSERT INTO multiple_uniq (a,c,b,d) +VALUES (11,3,2,14), (15,16,17,18) + +statement ok +PREPARE p1 AS INSERT INTO multiple_uniq +VALUES ($1, $1, $1, $1), ($2, $2, $2, $2) + +# A table with multiple unique without index checks is eligible for parameterized insert fast +# path. Verify it catches the duplicate value of (a). +statement error pq: duplicate key value violates unique constraint "unique_a"\nDETAIL: Key \(a\)=\(1\) already exists\. +EXECUTE p1(1, 2) + +statement ok +ALTER TABLE multiple_uniq ADD CONSTRAINT uniq_fk FOREIGN KEY (d) REFERENCES uniq (k) ON DELETE CASCADE; + +# A table with multiple unique without index checks and an FK constraint is +# eligible for insert fast path. Verify it catches the foreign key violation on +# (d). +statement error pq: insert on table "multiple_uniq" violates foreign key constraint "uniq_fk"\nDETAIL: Key \(d\)=\(14\) is not present in table "uniq"\. +INSERT INTO multiple_uniq (a,b,c,d) +VALUES (11,12,13,14), (15,16,17,18) diff --git a/pkg/sql/opt/exec/execbuilder/BUILD.bazel b/pkg/sql/opt/exec/execbuilder/BUILD.bazel index ce5b57639490..30912f2fa1b1 100644 --- a/pkg/sql/opt/exec/execbuilder/BUILD.bazel +++ b/pkg/sql/opt/exec/execbuilder/BUILD.bazel @@ -21,7 +21,6 @@ go_library( "//pkg/sql/catalog/descpb", "//pkg/sql/execinfra", "//pkg/sql/lexbase", - "//pkg/sql/mutations", "//pkg/sql/opt", "//pkg/sql/opt/cat", "//pkg/sql/opt/constraint", diff --git a/pkg/sql/opt/exec/execbuilder/builder.go b/pkg/sql/opt/exec/execbuilder/builder.go index 48b52702ea3b..57e9cd4a545b 100644 --- a/pkg/sql/opt/exec/execbuilder/builder.go +++ b/pkg/sql/opt/exec/execbuilder/builder.go @@ -298,7 +298,7 @@ func (b *Builder) build(e opt.Expr) (_ execPlan, err error) { ) } - canAutoCommit := b.canAutoCommit(rel) + canAutoCommit := memo.CanAutoCommit(rel) b.allowAutoCommit = b.allowAutoCommit && canAutoCommit // First condition from ConstructFastPathInsert: diff --git a/pkg/sql/opt/exec/execbuilder/mutation.go b/pkg/sql/opt/exec/execbuilder/mutation.go index f24c62ae12aa..30b793512626 100644 --- a/pkg/sql/opt/exec/execbuilder/mutation.go +++ b/pkg/sql/opt/exec/execbuilder/mutation.go @@ -16,7 +16,6 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/catalog/colinfo" "github.com/cockroachdb/cockroach/pkg/sql/lexbase" - "github.com/cockroachdb/cockroach/pkg/sql/mutations" "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/exec" @@ -144,28 +143,60 @@ func (b *Builder) tryBuildFastPathInsert(ins *memo.InsertExpr) (_ execPlan, ok b return execPlan{}, false, nil } - // - the input is Values with at most mutations.MaxBatchSize, and there are no - // subqueries; - // (note that mutations.MaxBatchSize() is a quantity of keys in the batch - // that we send, not a number of rows. We use this as a guideline only, - // and there is no guarantee that we won't produce a bigger batch.) - values, ok := ins.Input.(*memo.ValuesExpr) - if !ok || - values.ChildCount() > mutations.MaxBatchSize(false /* forceProductionMaxBatchSize */) || - values.Relational().HasSubquery || - values.Relational().HasUDF { - return execPlan{}, false, nil - } - - // We cannot use the fast path if any uniqueness checks are needed. - // TODO(rytaft): try to relax this restriction (see #58047). - if len(ins.UniqueChecks) > 0 { + insInput := ins.Input + values, ok := insInput.(*memo.ValuesExpr) + // Values expressions containing subqueries or UDFs, or having a size larger + // than the max mutation batch size are disallowed. + if !ok || !memo.ValuesLegalForInsertFastPath(values) { return execPlan{}, false, nil } md := b.mem.Metadata() tab := md.Table(ins.Table) + uniqChecks := make([]exec.InsertFastPathCheck, len(ins.UniqueChecks)) + for i := range ins.FastPathUniqueChecks { + c := &ins.FastPathUniqueChecks[i] + if len(c.DatumsFromConstraint) == 0 { + // We need at least one DatumsFromConstraint in order to perform + // uniqueness checks during fast-path insert. Even if DatumsFromConstraint + // contains no Datums, that case indicates that all values to check come + // from the input row. + return execPlan{}, false, nil + } + execFastPathCheck := &uniqChecks[i] + // Set up the execbuilder structure from the elements built during + // exploration. + execFastPathCheck.ReferencedTable = md.Table(c.ReferencedTableID) + execFastPathCheck.ReferencedIndex = execFastPathCheck.ReferencedTable.Index(c.ReferencedIndexOrdinal) + execFastPathCheck.CheckOrdinal = c.CheckOrdinal + locking, err := b.buildLocking(c.Locking) + if err != nil { + return execPlan{}, false, err + } + execFastPathCheck.Locking = locking + execFastPathCheck.InsertCols = make([]exec.TableColumnOrdinal, len(c.InsertCols)) + for j, insertCol := range c.InsertCols { + execFastPathCheck.InsertCols[j] = exec.TableColumnOrdinal(md.ColumnMeta(insertCol).Table.ColumnOrdinal(insertCol)) + } + datumsFromConstraintSpec := c.DatumsFromConstraint + execFastPathCheck.DatumsFromConstraint = make([]tree.Datums, len(datumsFromConstraintSpec)) + for j, row := range datumsFromConstraintSpec { + execFastPathCheck.DatumsFromConstraint[j] = make(tree.Datums, tab.ColumnCount()) + tuple := row.(*memo.TupleExpr) + if len(c.InsertCols) != len(tuple.Elems) { + panic(errors.AssertionFailedf("expected %d tuple elements in insert fast path uniqueness check, found %d", len(c.InsertCols), len(tuple.Elems))) + } + for k := 0; k < len(tuple.Elems); k++ { + constExpr, _ := tuple.Elems[k].(*memo.ConstExpr) + execFastPathCheck.DatumsFromConstraint[j][execFastPathCheck.InsertCols[k]] = constExpr.Value + } + } + execFastPathCheck.MkErr = func(values tree.Datums) error { + return mkFastPathUniqueCheckErr(md, &ins.UniqueChecks[i], values, execFastPathCheck.ReferencedIndex) + } + } + // - there are no self-referencing foreign keys; // - all FK checks can be performed using direct lookups into unique indexes. fkChecks := make([]exec.InsertFastPathCheck, len(ins.FKChecks)) @@ -276,6 +307,7 @@ func (b *Builder) tryBuildFastPathInsert(ins *memo.InsertExpr) (_ execPlan, ok b returnOrds, checkOrds, fkChecks, + uniqChecks, b.allowAutoCommit, ) if err != nil { @@ -818,6 +850,42 @@ func mkUniqueCheckErr(md *opt.Metadata, c *memo.UniqueChecksItem, keyVals tree.D ) } +// mkFastPathUniqueCheckErr is a wrapper for mkUniqueCheckErr in the insert fast +// path flow, which reorders the keyVals row according to the ordering of the +// key columns in index `idx`. This is needed because mkUniqueCheckErr assumes +// the ordering of columns in `keyVals` matches the ordering of columns in +// `cat.UniqueConstraint.ColumnOrdinal(tabMeta.Table, i)`. +func mkFastPathUniqueCheckErr( + md *opt.Metadata, c *memo.UniqueChecksItem, keyVals tree.Datums, idx cat.Index, +) error { + + tabMeta := md.TableMeta(c.Table) + uc := tabMeta.Table.Unique(c.CheckOrdinal) + + newKeyVals := make(tree.Datums, 0, uc.ColumnCount()) + + for i := 0; i < uc.ColumnCount(); i++ { + ord := uc.ColumnOrdinal(tabMeta.Table, i) + found := false + for j := 0; j < idx.ColumnCount(); j++ { + keyCol := idx.Column(j) + keyColOrd := keyCol.Column.Ordinal() + if ord == keyColOrd { + newKeyVals = append(newKeyVals, keyVals[j]) + found = true + break + } + } + if !found { + // We still need to return an error, even if the key values could not be + // determined. + return errors.AssertionFailedf( + "insert fast path failed uniqueness check, but could not find unique columns for row, %v", keyVals) + } + } + return mkUniqueCheckErr(md, c, newKeyVals) +} + // mkFKCheckErr generates a user-friendly error describing a foreign key // violation. The keyVals are the values that correspond to the // cat.ForeignKeyConstraint columns. @@ -925,59 +993,6 @@ func (b *Builder) buildFKCascades(withID opt.WithID, cascades memo.FKCascades) e return nil } -// canAutoCommit determines if it is safe to auto commit the mutation contained -// in the expression. -// -// Mutations can commit the transaction as part of the same KV request, -// potentially taking advantage of the 1PC optimization. This is not ok to do in -// general; a sufficient set of conditions is: -// 1. There is a single mutation in the query. -// 2. The mutation is the root operator, or it is directly under a Project -// with no side-effecting expressions. An example of why we can't allow -// side-effecting expressions: if the projection encounters a -// division-by-zero error, the mutation shouldn't have been committed. -// -// An extra condition relates to how the FK checks are run. If they run before -// the mutation (via the insert fast path), auto commit is possible. If they run -// after the mutation (the general path), auto commit is not possible. It is up -// to the builder logic for each mutation to handle this. -// -// Note that there are other necessary conditions related to execution -// (specifically, that the transaction is implicit); it is up to the exec -// factory to take that into account as well. -func (b *Builder) canAutoCommit(rel memo.RelExpr) bool { - if !rel.Relational().CanMutate { - // No mutations in the expression. - return false - } - - switch rel.Op() { - case opt.InsertOp, opt.UpsertOp, opt.UpdateOp, opt.DeleteOp: - // Check that there aren't any more mutations in the input. - // TODO(radu): this can go away when all mutations are under top-level - // With ops. - return !rel.Child(0).(memo.RelExpr).Relational().CanMutate - - case opt.ProjectOp: - // Allow Project on top, as long as the expressions are not side-effecting. - proj := rel.(*memo.ProjectExpr) - for i := 0; i < len(proj.Projections); i++ { - if !proj.Projections[i].ScalarProps().VolatilitySet.IsLeakproof() { - return false - } - } - return b.canAutoCommit(proj.Input) - - case opt.DistributeOp: - // Distribute is currently a no-op, so check whether the input can - // auto-commit. - return b.canAutoCommit(rel.(*memo.DistributeExpr).Input) - - default: - return false - } -} - // forUpdateLocking is the row-level locking mode used by mutations during their // initial row scan, when such locking is deemed desirable. The locking mode is // equivalent to that used by a SELECT FOR UPDATE statement, except not durable. diff --git a/pkg/sql/opt/exec/execbuilder/testdata/unique b/pkg/sql/opt/exec/execbuilder/testdata/unique index a09a7dff281c..c0802ac512c8 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/unique +++ b/pkg/sql/opt/exec/execbuilder/testdata/unique @@ -4784,5 +4784,608 @@ vectorized: true estimated row count: 1 label: buffer 1 +subtest insertFastPathUniqueChecks + +statement ok +CREATE TABLE t1 ( + pk int PRIMARY KEY, + pk2 int, + a int, + b int, + j JSON, + INDEX (a), + UNIQUE WITHOUT INDEX(b, a), + INDEX (b,a) STORING(pk2, j), + INVERTED INDEX (j), + FAMILY (pk, pk2, a, b) +) + +# Insert of multi-row VALUES into table with UNIQUE WITHOUT INDEX and a +# usable index on a prefix of those columns cannot currently use insert +# fast-path. +query T +EXPLAIN +INSERT INTO t1 (pk, pk2, a, b, j) VALUES + (2, 2, 1, 3, '{"a": "b"}'), (4, 5, 6, 7, '{"a": "b"}'); +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: t1(pk, pk2, a, b, j) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 5 columns, 2 rows +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: t1@t1_b_a_idx + │ equality: (column4, column3) = (b,a) + │ pred: column1 != pk + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +# Insert of single-row VALUES into table with UNIQUE WITHOUT INDEX and a +# usable index on a prefix of those columns can use insert fast-path. +query T +EXPLAIN +INSERT INTO t1 (pk, pk2, a, b, j) VALUES + (2, 2, 1, 3, '{"a": "b"}'); +---- +distribution: local +vectorized: true +· +• insert fast path + into: t1(pk, pk2, a, b, j) + auto commit + uniqueness check: t1@t1_b_a_idx + size: 5 columns, 1 row + +statement ok +CREATE TABLE users ( + id UUID DEFAULT gen_random_uuid(), + name STRING NOT NULL, + email STRING NOT NULL, + UNIQUE WITHOUT INDEX (email) WHERE email != name, + PRIMARY KEY (email, id) +) + +# Verify a partial UNIQUE WITHOUT INDEX with a predicate similar to the PK +# values check created in `buildInsertionCheck` to prevent rows from matching +# themselves doesn't mistakenly allow insert fast path. +query T +EXPLAIN INSERT INTO users (name, email) +VALUES ('Craig Roacher', 'craig@cockroachlabs.com') +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: users(id, name, email) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 3 columns, 1 row +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: users@users_pkey + │ equality: (column2) = (email) + │ pred: (id_default != id) AND (email != name) + │ + └── • filter + │ estimated row count: 0 + │ filter: column2 != column1 + │ + └── • scan buffer + estimated row count: 1 + label: buffer 1 + +statement ok +CREATE TABLE multiple_uniq ( + a INT, + b INT, + c INT, + d INT, + UNIQUE WITHOUT INDEX (b, c), + UNIQUE WITHOUT INDEX (a, b, d), + UNIQUE WITHOUT INDEX (a), + INDEX (a, b, d), + INDEX (b, c), + FAMILY (a), + FAMILY (b), + FAMILY (c), + FAMILY (d) +) + +# A table with multiple unique without index checks is not currently eligible +# for multi-row insert fast path. +query T +EXPLAIN INSERT INTO multiple_uniq +VALUES (1,1,1,1), (2,2,2,2) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: multiple_uniq(a, b, c, d, rowid) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 4 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_b_c_idx +│ │ equality: (column2, column3) = (b,c) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_a_b_d_idx +│ │ equality: (column1, column2, column4) = (a,b,d) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (semi) + │ table: multiple_uniq@multiple_uniq_a_b_d_idx + │ equality: (column1) = (a) + │ pred: rowid_default != rowid + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +# A table with multiple unique without index checks is eligible for insert fast +# path of a single row. +query T +EXPLAIN INSERT INTO multiple_uniq +VALUES (1,1,1,1) +---- +distribution: local +vectorized: true +· +• insert fast path + into: multiple_uniq(a, b, c, d, rowid) + auto commit + uniqueness check: multiple_uniq@multiple_uniq_b_c_idx + uniqueness check: multiple_uniq@multiple_uniq_a_b_d_idx + uniqueness check: multiple_uniq@multiple_uniq_a_b_d_idx + size: 5 columns, 1 row + +statement ok +ALTER TABLE multiple_uniq ADD CONSTRAINT uniq_fk FOREIGN KEY (d) REFERENCES uniq (k) ON DELETE CASCADE; + +# A table with multiple unique without index checks and an FK constraint is +# not currently eligible for multi-row insert fast path. +query T +EXPLAIN INSERT INTO multiple_uniq +VALUES (1,1,1,1), (2,2,2,2) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: multiple_uniq(a, b, c, d, rowid) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 4 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_b_c_idx +│ │ equality: (column2, column3) = (b,c) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_a_b_d_idx +│ │ equality: (column1, column2, column4) = (a,b,d) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_a_b_d_idx +│ │ equality: (column1) = (a) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (anti) + │ table: uniq@uniq_pkey + │ equality: (column4) = (k) + │ equality cols are key + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +# A table with multiple unique without index checks and an FK constraint is +# eligible for insert fast path of a single row. +query T +EXPLAIN INSERT INTO multiple_uniq +VALUES (1,1,1,1) +---- +distribution: local +vectorized: true +· +• insert fast path + into: multiple_uniq(a, b, c, d, rowid) + auto commit + FK check: uniq@uniq_pkey + uniqueness check: multiple_uniq@multiple_uniq_b_c_idx + uniqueness check: multiple_uniq@multiple_uniq_a_b_d_idx + uniqueness check: multiple_uniq@multiple_uniq_a_b_d_idx + size: 5 columns, 1 row + +statement ok +PREPARE e1 AS EXPLAIN INSERT INTO multiple_uniq +VALUES ($1, $1, $1, $1), ($2, $2, $2, $2) + +# A table with multiple unique without index checks and an FK constraint is +# not currently eligible for parameterized multi-row insert fast path. +query T nosort +EXECUTE e1(1, 2) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: multiple_uniq(a, b, c, d, rowid) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • render +│ │ +│ └── • values +│ size: 4 columns, 2 rows +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_b_c_idx +│ │ equality: (column2, column3) = (b,c) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_a_b_d_idx +│ │ equality: (column1, column2, column4) = (a,b,d) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • lookup join (semi) +│ │ table: multiple_uniq@multiple_uniq_a_b_d_idx +│ │ equality: (column1) = (a) +│ │ pred: rowid_default != rowid +│ │ +│ └── • scan buffer +│ estimated row count: 2 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • lookup join (anti) + │ table: uniq@uniq_pkey + │ equality: (column4) = (k) + │ equality cols are key + │ + └── • scan buffer + estimated row count: 2 + label: buffer 1 + +statement ok +PREPARE e2 AS EXPLAIN INSERT INTO multiple_uniq +VALUES ($1, $1, $1, $1) + +# A table with multiple unique without index checks and an FK constraint is +# eligible for parameterized single-row insert fast path. +query T nosort +EXECUTE e2(1) +---- +distribution: local +vectorized: true +· +• insert fast path + into: multiple_uniq(a, b, c, d, rowid) + auto commit + FK check: uniq@uniq_pkey + uniqueness check: multiple_uniq@multiple_uniq_b_c_idx + uniqueness check: multiple_uniq@multiple_uniq_a_b_d_idx + uniqueness check: multiple_uniq@multiple_uniq_a_b_d_idx + size: 5 columns, 1 row + +statement ok +CREATE TABLE multiple_uniq_partial ( + a INT, + b INT, + c INT, + d INT, + UNIQUE WITHOUT INDEX (b, c), + UNIQUE WITHOUT INDEX (a, b, d), + UNIQUE WITHOUT INDEX (a), + INDEX (a, b, d) WHERE a > 0, + INDEX (b, c) WHERE b > 0, + FAMILY (a), + FAMILY (b), + FAMILY (c), + FAMILY (d) +) + +# A table with multiple unique without index checks and only partial indexes +# to cover them is eligible for insert fast path if it can be statically +# determined legal to use the indexes. +query T +EXPLAIN INSERT INTO multiple_uniq_partial +VALUES (1,1,1,1) +---- +distribution: local +vectorized: true +· +• insert fast path + into: multiple_uniq_partial(a, b, c, d, rowid) + auto commit + uniqueness check: multiple_uniq_partial@multiple_uniq_partial_b_c_idx + uniqueness check: multiple_uniq_partial@multiple_uniq_partial_a_b_d_idx + uniqueness check: multiple_uniq_partial@multiple_uniq_partial_a_b_d_idx + size: 7 columns, 1 row + +# A table with multiple unique without index checks and only partial indexes +# to cover them is eligible for insert fast path if it is determined, +# given the insert row, illegal to use the indexes. +query T +EXPLAIN INSERT INTO multiple_uniq_partial +VALUES (0,0,0,0) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: multiple_uniq_partial(a, b, c, d, rowid) +│ │ +│ └── • buffer +│ │ label: buffer 1 +│ │ +│ └── • values +│ size: 7 columns, 1 row +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (right semi) +│ │ equality: (b, c) = (column2, column3) +│ │ right cols are key +│ │ pred: rowid_default != rowid +│ │ +│ ├── • scan +│ │ missing stats +│ │ table: multiple_uniq_partial@multiple_uniq_partial_pkey +│ │ spans: FULL SCAN +│ │ +│ └── • scan buffer +│ estimated row count: 1 +│ label: buffer 1 +│ +├── • constraint-check +│ │ +│ └── • error if rows +│ │ +│ └── • hash join (right semi) +│ │ equality: (a, b, d) = (column1, column2, column4) +│ │ right cols are key +│ │ pred: rowid_default != rowid +│ │ +│ ├── • scan +│ │ missing stats +│ │ table: multiple_uniq_partial@multiple_uniq_partial_pkey +│ │ spans: FULL SCAN +│ │ +│ └── • scan buffer +│ estimated row count: 1 +│ label: buffer 1 +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • hash join (right semi) + │ equality: (a) = (column1) + │ right cols are key + │ pred: rowid_default != rowid + │ + ├── • scan + │ missing stats + │ table: multiple_uniq_partial@multiple_uniq_partial_pkey + │ spans: FULL SCAN + │ + └── • scan buffer + estimated row count: 1 + label: buffer 1 + +statement ok +CREATE TABLE t ( + k INT PRIMARY KEY, + r STRING NOT NULL, + a INT, + b INT, + UNIQUE (r, a), + UNIQUE WITHOUT INDEX (a) WHERE k != 1, + CHECK (r IN ('east', 'west')) +) + +statement ok +CREATE TABLE tt ( + k INT PRIMARY KEY, + r STRING NOT NULL, + a INT, + b INT, + UNIQUE (r, a) WHERE k != 1, + UNIQUE WITHOUT INDEX (a) WHERE k != 1, + CHECK (r IN ('east', 'west')) +) + +# This case cannot use insert fast path because of the partial unique without +# index. +query T +EXPLAIN +INSERT INTO tt VALUES (2, 'east', 10, 100) +---- +distribution: local +vectorized: true +· +• root +│ +├── • insert +│ │ into: tt(k, r, a, b) +│ │ +│ └── • values +│ size: 6 columns, 1 row +│ +└── • constraint-check + │ + └── • error if rows + │ + └── • cross join + │ + ├── • values + │ size: 1 column, 1 row + │ + └── • limit + │ count: 1 + │ + └── • filter + │ filter: k != 2 + │ + └── • scan + missing stats + table: tt@tt_r_a_key (partial index) + spans: [/'east'/10 - /'east'/10] [/'west'/10 - /'west'/10] + +# Mimic a REGIONAL BY ROW table using a CHECK constraint. +statement ok +CREATE TABLE t11 ( + pk int PRIMARY KEY, + pk2 int, + a int, + b int CHECK (b IN (1,2,3,4,5)), + j JSON, + INDEX (a), + UNIQUE WITHOUT INDEX(a), + UNIQUE INDEX (b,a) STORING(pk2, j), + INVERTED INDEX (j), + FAMILY (pk, pk2, a, b) +) + +# Insert of single-row VALUES into table with UNIQUE WITHOUT INDEX which +# mimics a REGIONAL BY ROW table with a CHECK constraint can use insert +# fast-path. +query T +EXPLAIN +INSERT INTO t11 (pk, pk2, a, b, j) VALUES + (2, 2, 1, 3, '{"a": "b"}'); +---- +distribution: local +vectorized: true +· +• insert fast path + into: t11(pk, pk2, a, b, j) + auto commit + uniqueness check: t11@t11_a_idx + size: 6 columns, 1 row + statement ok SET CLUSTER SETTING sql.optimizer.uniqueness_checks_for_gen_random_uuid.enabled = false diff --git a/pkg/sql/opt/exec/explain/emit.go b/pkg/sql/opt/exec/explain/emit.go index bd3134cf12e3..c1c8f044ce56 100644 --- a/pkg/sql/opt/exec/explain/emit.go +++ b/pkg/sql/opt/exec/explain/emit.go @@ -871,6 +871,12 @@ func (e *emitter) emitNodeAttributes(n *Node) error { ) e.emitLockingPolicyWithPrefix("FK check ", fk.Locking) } + for _, uniq := range a.UniqChecks { + ob.Attr( + "uniqueness check", fmt.Sprintf("%s@%s", uniq.ReferencedTable.Name(), uniq.ReferencedIndex.Name()), + ) + e.emitLockingPolicyWithPrefix("uniqueness check ", uniq.Locking) + } if len(a.Rows) > 0 { e.emitTuples(tree.RawRows(a.Rows), len(a.Rows[0])) } diff --git a/pkg/sql/opt/exec/factory.go b/pkg/sql/opt/exec/factory.go index e40ccbc4c521..0935c4b3a86a 100644 --- a/pkg/sql/opt/exec/factory.go +++ b/pkg/sql/opt/exec/factory.go @@ -257,6 +257,9 @@ type InsertFastPathCheck struct { ReferencedTable cat.Table ReferencedIndex cat.Index + // This is the ordinal of the check in the table's unique constraints. + CheckOrdinal int + // InsertCols contains the table column ordinals of the referenced index key // columns. The position in this slice corresponds with the ordinal of the // referenced index column (its position in the index key). diff --git a/pkg/sql/opt/exec/factory.opt b/pkg/sql/opt/exec/factory.opt index ecd6771029eb..ce8c34097923 100644 --- a/pkg/sql/opt/exec/factory.opt +++ b/pkg/sql/opt/exec/factory.opt @@ -464,10 +464,13 @@ define Insert { # insert is not processed through side-effecting expressions. # - there are no self-referencing foreign keys; # - all FK checks can be performed using direct lookups into unique indexes. +# - all uniqueness checks can be performed using direct lookups +# into an index (could be a key prefix and not the entire key). # -# In this case, the foreign-key checks can run before (or even concurrently -# with) the insert. If they are run before, the insert is allowed to -# auto-commit. +# In this case, the foreign-key and uniqueness checks can run before the insert. +# Foreign-key checks could potentially be run concurrently with the insert since +# they are scanning from a different index than is being mutated. If the checks +# are run before, the insert is allowed to auto-commit. define InsertFastPath { Rows [][]tree.TypedExpr Table cat.Table @@ -475,6 +478,7 @@ define InsertFastPath { ReturnCols exec.TableColumnOrdinalSet CheckCols exec.CheckOrdinalSet FkChecks []exec.InsertFastPathCheck + UniqChecks []exec.InsertFastPathCheck # If set, the operator will commit the transaction as part of its execution. # This is false when executing inside an explicit transaction. diff --git a/pkg/sql/opt/memo/BUILD.bazel b/pkg/sql/opt/memo/BUILD.bazel index 9203a1654c52..1b6959f7e7db 100644 --- a/pkg/sql/opt/memo/BUILD.bazel +++ b/pkg/sql/opt/memo/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "extract.go", "filters_expr_mutate_checker.go", "group.go", + "insert_fastpath.go", "interner.go", "logical_props_builder.go", "memo.go", @@ -29,6 +30,7 @@ go_library( "//pkg/sql/catalog/colinfo", "//pkg/sql/catalog/tabledesc", "//pkg/sql/inverted", + "//pkg/sql/mutations", "//pkg/sql/opt", "//pkg/sql/opt/cat", "//pkg/sql/opt/constraint", diff --git a/pkg/sql/opt/memo/insert_fastpath.go b/pkg/sql/opt/memo/insert_fastpath.go new file mode 100644 index 000000000000..0484aed9d6c9 --- /dev/null +++ b/pkg/sql/opt/memo/insert_fastpath.go @@ -0,0 +1,90 @@ +// Copyright 2023 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. + +// This module holds functions related to building insert fast path +// foreign key and uniqueness checks and deal mostly with structures +// defined in the memo package. + +package memo + +import ( + "github.com/cockroachdb/cockroach/pkg/sql/mutations" + "github.com/cockroachdb/cockroach/pkg/sql/opt" +) + +// ValuesLegalForInsertFastPath tests if `values` is a Values expression that +// has no subqueries or UDFs and has less rows than the max number of entries in +// a KV batch for a mutation operation. +func ValuesLegalForInsertFastPath(values *ValuesExpr) bool { + // - The input is Values with at most mutations.MaxBatchSize, and there are no + // subqueries; + // (note that mutations.MaxBatchSize() is a quantity of keys in the batch + // that we send, not a number of rows. We use this as a guideline only, + // and there is no guarantee that we won't produce a bigger batch.) + if values.ChildCount() > mutations.MaxBatchSize(false /* forceProductionMaxBatchSize */) || + values.Relational().HasSubquery || + values.Relational().HasUDF { + return false + } + return true +} + +// CanAutoCommit determines if it is safe to auto commit the mutation contained +// in the expression. +// +// Mutations can commit the transaction as part of the same KV request, +// potentially taking advantage of the 1PC optimization. This is not ok to do in +// general; a sufficient set of conditions is: +// 1. There is a single mutation in the query. +// 2. The mutation is the root operator, or it is directly under a Project +// with no side-effecting expressions. An example of why we can't allow +// side-effecting expressions: if the projection encounters a +// division-by-zero error, the mutation shouldn't have been committed. +// +// An extra condition relates to how the FK checks are run. If they run before +// the mutation (via the insert fast path), auto commit is possible. If they run +// after the mutation (the general path), auto commit is not possible. It is up +// to the builder logic for each mutation to handle this. +// +// Note that there are other necessary conditions related to execution +// (specifically, that the transaction is implicit); it is up to the exec +// factory to take that into account as well. +func CanAutoCommit(rel RelExpr) bool { + if !rel.Relational().CanMutate { + // No mutations in the expression. + return false + } + + switch rel.Op() { + case opt.InsertOp, opt.UpsertOp, opt.UpdateOp, opt.DeleteOp: + // Check that there aren't any more mutations in the input. + // TODO(radu): this can go away when all mutations are under top-level + // With ops. + return !rel.Child(0).(RelExpr).Relational().CanMutate + + case opt.ProjectOp: + // Allow Project on top, as long as the expressions are not side-effecting. + proj := rel.(*ProjectExpr) + for i := 0; i < len(proj.Projections); i++ { + if !proj.Projections[i].ScalarProps().VolatilitySet.IsLeakproof() { + return false + } + } + return CanAutoCommit(proj.Input) + + case opt.DistributeOp: + // Distribute is currently a no-op, so check whether the input can + // auto-commit. + return CanAutoCommit(rel.(*DistributeExpr).Input) + + default: + return false + } +} diff --git a/pkg/sql/opt/optbuilder/mutation_builder_unique.go b/pkg/sql/opt/optbuilder/mutation_builder_unique.go index f31317599818..3ebc3bafdb63 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder_unique.go +++ b/pkg/sql/opt/optbuilder/mutation_builder_unique.go @@ -396,12 +396,18 @@ func (h *uniqueCheckHelper) buildFiltersForFastPathCheck( // This is currently not supported. return nil } - scanFilters = append(scanFilters, f.ConstructFiltersItem( + filtersItem := f.ConstructFiltersItem( f.ConstructEq( f.ConstructVariable(h.scanScope.cols[i].id), tupleScalarExpression, ), - )) + ) + // Volatile expressions may return a different value on each evaluation, + // so must be disabled for this check as the actual insert value may differ. + if filtersItem.ScalarProps().VolatilitySet.HasVolatile() { + return nil + } + scanFilters = append(scanFilters, filtersItem) } return scanFilters } @@ -539,6 +545,8 @@ func (h *uniqueCheckHelper) buildInsertionCheck( if foundScan && len(scanFilters) != 0 { newScanScope, _ := h.buildTableScan() newPossibleScan := newScanScope.expr + // Hash-sharded REGIONAL BY ROW tables may include a projection which can + // be skipped over to find the applicable Scan. if skipProjectExpr, ok := newPossibleScan.(*memo.ProjectExpr); ok { newPossibleScan = skipProjectExpr.Input } diff --git a/pkg/sql/opt/xform/BUILD.bazel b/pkg/sql/opt/xform/BUILD.bazel index ece0c661228f..c384ff01da99 100644 --- a/pkg/sql/opt/xform/BUILD.bazel +++ b/pkg/sql/opt/xform/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "general_funcs.go", "groupby_funcs.go", "index_scan_builder.go", + "insert_funcs.go", "join_funcs.go", "join_order_builder.go", "limit_funcs.go", diff --git a/pkg/sql/opt/xform/coster.go b/pkg/sql/opt/xform/coster.go index dc61f0ce9e3a..83d618920ed9 100644 --- a/pkg/sql/opt/xform/coster.go +++ b/pkg/sql/opt/xform/coster.go @@ -593,6 +593,17 @@ func (c *coster) ComputeCost(candidate memo.RelExpr, required *physical.Required case opt.ProjectSetOp: cost = c.computeProjectSetCost(candidate.(*memo.ProjectSetExpr)) + case opt.InsertOp: + insertExpr, _ := candidate.(*memo.InsertExpr) + if len(insertExpr.UniqueChecks) != 0 { + if len(insertExpr.FastPathUniqueChecks[0].DatumsFromConstraint) != 0 { + // Make the cost of insert fast path slightly cheaper than non-fast path + // so that the optimizer will pick it. All of the costed operations + // should have identical costs between the two inserts. + cost -= cpuCostFactor + } + } + case opt.ExplainOp: // Technically, the cost of an Explain operation is independent of the cost // of the underlying plan. However, we want to explain the plan we would get diff --git a/pkg/sql/opt/xform/insert_funcs.go b/pkg/sql/opt/xform/insert_funcs.go new file mode 100644 index 000000000000..4764b784c864 --- /dev/null +++ b/pkg/sql/opt/xform/insert_funcs.go @@ -0,0 +1,179 @@ +// Copyright 2023 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. + +// Package xform contains logic for transforming SQL queries. +package xform + +import ( + "github.com/cockroachdb/cockroach/pkg/sql/opt" + "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" + "github.com/cockroachdb/cockroach/pkg/sql/types" +) + +// CanUseUniqueChecksForInsertFastPath analyzes the `uniqueChecks` in an Insert +// of values and determines if they allow the insert to be executed in fast path +// mode, which performs the uniqueness checks in a batched KV operation. If +// insert fast path is legal, `DatumsFromConstraint` and other items in the +// private section of each `FastPathUniqueChecksItem` in `FastPathUniqueChecks` +// is constructed with information needed to build fast path insert in the +// execbuilder, and returned to the caller via `newFastPathUniqueChecks` for +// constructing a new InsertExpr. Note that the execbuilder may still choose not +// to pick insert fast path if the statement no longer qualifies, for example if +// autocommit can't be done or if the session explicitly disables insert fast +// path. +func (c *CustomFuncs) CanUseUniqueChecksForInsertFastPath( + ins *memo.InsertExpr, +) (newFastPathUniqueChecks memo.FastPathUniqueChecksExpr, ok bool) { + + fastPathUniqueChecks := ins.FastPathUniqueChecks + if len(fastPathUniqueChecks) == 0 { + return memo.FastPathUniqueChecksExpr{}, false + } + if len(fastPathUniqueChecks[0].DatumsFromConstraint) != 0 { + // Fast path checks have already been built. + return memo.FastPathUniqueChecksExpr{}, false + } + planner := c.e.evalCtx.Planner + if planner == nil { + // The planner can actually be nil in some tests. Avoid errors trying to + // access a nil planner in different functions called below. + return memo.FastPathUniqueChecksExpr{}, false + } + canAutoCommit := memo.CanAutoCommit(ins) + if !canAutoCommit { + // Don't bother building fast path structures for a statement which won't + // qualify for fast path in the execbuilder. + return memo.FastPathUniqueChecksExpr{}, false + } + + sd := c.e.evalCtx.SessionData() + if !sd.InsertFastPath { + // Insert fast path is explicitly disabled. Skip the work of building + // structures for it. + return memo.FastPathUniqueChecksExpr{}, false + } + + // See comments in execbuilder.New() for why autocommit might be prohibited. + // Insert fast path relies on using autocommit. + prohibitAutoCommit := sd.TxnRowsReadErr != 0 && !sd.Internal + if prohibitAutoCommit { + // Don't bother building fast path structures for a statement which won't + // qualify for fast path in the execbuilder. + return memo.FastPathUniqueChecksExpr{}, false + } + + insInput := ins.Input + values, ok := insInput.(*memo.ValuesExpr) + // Values expressions containing subqueries or UDFs, or having a size larger + // than the max mutation batch size are disallowed. + if !ok || !memo.ValuesLegalForInsertFastPath(values) { + return memo.FastPathUniqueChecksExpr{}, false + } + for i := range ins.FastPathUniqueChecks { + fastPathUniqueChecksItem := &ins.FastPathUniqueChecks[i] + check := fastPathUniqueChecksItem.Check + checkOrdinal := fastPathUniqueChecksItem.CheckOrdinal + + // Skip over distribute and projection operations. + if distributeExpr, isDistribute := check.(*memo.DistributeExpr); isDistribute { + check = distributeExpr.Input + } + if private, ok := c.handleSingleRowInsert(check, checkOrdinal); ok { + // Build the qualified FastPathCheck into a new UniqueChecksItem for the + // new InsertExpr being constructed. + newFastPathUniqueChecksItem := c.e.f.ConstructFastPathUniqueChecksItem( + fastPathUniqueChecksItem.Check, + private, + ) + newFastPathUniqueChecks = append(newFastPathUniqueChecks, newFastPathUniqueChecksItem) + continue + } + return memo.FastPathUniqueChecksExpr{}, false + } + return newFastPathUniqueChecks, true +} + +// handleSingleRowInsert examines an insert fast path index `check` relation +// which was specially constructed to find a constrained index scan which fully +// consumes all filters needed to perform the uniqueness check in +// `uniqueChecksItem`, and builds the corresponding `private` return value of +// type `FastPathUniqueChecksItemPrivate`. This struct is used to drive insert +// fast path unique constraint checks in the execution engine. +func (c *CustomFuncs) handleSingleRowInsert( + check memo.RelExpr, checkOrdinal int, +) (private *memo.FastPathUniqueChecksItemPrivate, ok bool) { + md := c.e.f.Memo().Metadata() + + var scanExpr *memo.ScanExpr + for check = check.FirstExpr(); check != nil; check = check.NextExpr() { + expr := check + + if indexJoinExpr, isIndexJoin := expr.(*memo.IndexJoinExpr); isIndexJoin { + expr = indexJoinExpr.Input + } + if scan, isScan := expr.(*memo.ScanExpr); isScan { + // We need a scan with a constraint for analysis, and it should not be + // a contradiction (having no spans). + if scan.Constraint != nil && scan.Constraint.Spans.Count() != 0 { + scanExpr = scan + break + } + } + } + if check == nil { + return nil, false + } + referencedTable := md.Table(scanExpr.Table) + referencedIndex := referencedTable.Index(scanExpr.Index) + private = &memo.FastPathUniqueChecksItemPrivate{} + private.ReferencedTableID = scanExpr.Table + private.ReferencedIndexOrdinal = scanExpr.Index + private.CheckOrdinal = checkOrdinal + private.Locking = scanExpr.Locking + + // Set up InsertCols with the index key columns. + numKeyCols := scanExpr.Constraint.Spans.Get(0).StartKey().Length() + private.InsertCols = make(opt.ColList, numKeyCols) + columnOrdinals := make([]int, numKeyCols) + for j := 0; j < numKeyCols; j++ { + ord := referencedIndex.Column(j).Ordinal() + if scanExpr.Constraint.Columns.Get(j).ID() != scanExpr.Table.ColumnID(ord) { + // Check that the constraint column ids match those of the chosen index. + // Maybe this must always be true, but better to check and not allow any + // incorrect decisions. + return nil, false + } + columnOrdinals[j] = ord + private.InsertCols[j] = private.ReferencedTableID.ColumnID(ord) + } + // The number of KV requests will match the number of spans. + private.DatumsFromConstraint = make(memo.ScalarListExpr, scanExpr.Constraint.Spans.Count()) + for j := 0; j < scanExpr.Constraint.Spans.Count(); j++ { + span := scanExpr.Constraint.Spans.Get(j) + // Verify that the span has the same number of columns as the index key... + if span.StartKey().Length() != numKeyCols { + return nil, false + } + // ... and that the span has a single key. + if !span.HasSingleKey(c.e.evalCtx) { + return nil, false + } + keySpecification := make(memo.ScalarListExpr, span.StartKey().Length()) + elemTypes := make([]*types.T, span.StartKey().Length()) + for k := 0; k < span.StartKey().Length(); k++ { + // Populate DatumsFromConstraint with that key column value. + elemTypes[k] = span.StartKey().Value(k).ResolvedType() + keySpecification[k] = c.e.f.ConstructConstVal(span.StartKey().Value(k), elemTypes[k]) + } + private.DatumsFromConstraint[j] = c.e.f.ConstructTuple(keySpecification, types.MakeTuple(elemTypes)) + } + // We must build at least one DatumsFromConstraint entry to claim success. + return private, len(private.DatumsFromConstraint) != 0 +} diff --git a/pkg/sql/opt/xform/rules/insert.opt b/pkg/sql/opt/xform/rules/insert.opt new file mode 100644 index 000000000000..665b2562d26f --- /dev/null +++ b/pkg/sql/opt/xform/rules/insert.opt @@ -0,0 +1,33 @@ +# ============================================================================= +# insert.opt contains exploration rules for the Insert operator. +# ============================================================================= + +# InsertFastPath checks if an insert statement may qualify for executing fast +# path uniqueness checks, typically for unique indexes which internally use +# UNIQUE WITHOUT INDEX constraints to enforce uniqueness. A prime example of +# this is REGIONAL BY ROW tables, whose index keys are always prefixed by a +# `crdb_region` column, and so must check in all regions for duplicates when the +# `crdb_region` column is not explicitly included as a unique index key column. +# When `CanUseUniqueChecksForInsertFastPath` returns true, it also builds +# complete fast path uniqueness checks in $newFastPathUnique, which the +# execbuilder uses to build the actual insert fast path operation. This rewrite +# only applies to an insert of values, and not other forms like INSERT SELECT. +# If fast path information has already been built, this rule will not try to +# rewrite the insert a second time. +[InsertFastPath, Explore] +(Insert + $insInput:* + $unique:* + * & + (Let + ( + $newFastPathUnique + $ok + ):(CanUseUniqueChecksForInsertFastPath (Root)) + $ok + ) + $fk:* + $mutations:* +) +=> +(Insert $insInput $unique $newFastPathUnique $fk $mutations) diff --git a/pkg/sql/opt/xform/testdata/rules/insert b/pkg/sql/opt/xform/testdata/rules/insert index f309505d8579..b677009dd3d7 100644 --- a/pkg/sql/opt/xform/testdata/rules/insert +++ b/pkg/sql/opt/xform/testdata/rules/insert @@ -16,7 +16,7 @@ CREATE TABLE t ( # Fast path is not allowed when there is a UNIQUE WITHOUT INDEX with a # partial index predicate. -opt format=show-fastpathchecks +opt format=show-fastpathchecks expect-not=InsertFastPath INSERT INTO t VALUES (1, 'east', 10, 100) ---- insert t @@ -68,10 +68,13 @@ CREATE TABLE t ( ) ---- -# A computed leading index column allows fast path uniqueness checks. +# Fast path uniqueness checks may be created where there is a computed column, +# but insert fast path might not be chosen due to lack of usable indexes. # Note, the best-cost fast path check relation selected may not be one -# which would enable fast path uniqueness checks. -opt format=show-fastpathchecks +# which would enable fast path uniqueness checks, but logic in insert_funcs.go +# will cycle through all expressions in the memo group to try to find one +# which does. +opt format=show-fastpathchecks expect-not=InsertFastPath INSERT INTO t (k, r, a, c) VALUES (2, 'east', 10, 20) ---- insert t @@ -207,6 +210,225 @@ exec-ddl DROP TABLE t ---- +exec-ddl +CREATE TABLE t ( + k INT PRIMARY KEY, + r STRING NOT NULL, + a INT, + b INT AS (a % 9) STORED, + c INT, + INDEX (b, a), + INDEX (r, c), + UNIQUE WITHOUT INDEX (a), + UNIQUE WITHOUT INDEX (c), + CHECK (r IN ('east', 'west')) +) +---- + +# This case computes computed column `b` using the value specified for the unique +# check column `a`, so triggers insert fast path. +opt format=show-fastpathchecks expect=InsertFastPath +INSERT INTO t (k, r, a, c) VALUES (2, 'east', 10, 20) +---- +insert t + ├── columns: + ├── insert-mapping: + │ ├── column1:8 => t.k:1 + │ ├── column2:9 => t.r:2 + │ ├── column3:10 => t.a:3 + │ ├── b_comp:12 => t.b:4 + │ └── column4:11 => t.c:5 + ├── check columns: check1:13 + ├── cardinality: [0 - 0] + ├── volatile, mutations + ├── values + │ ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null b_comp:12!null check1:13!null + │ ├── cardinality: [1 - 1] + │ ├── key: () + │ ├── fd: ()-->(8-13) + │ └── (2, 'east', 10, 20, 1, true) + ├── unique-checks + │ ├── unique-checks-item: t(a) + │ │ └── project + │ │ ├── columns: a:23!null + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(23) + │ │ └── project + │ │ ├── columns: a:23!null + │ │ ├── cardinality: [0 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(23) + │ │ └── inner-join (cross) + │ │ ├── columns: t.k:14!null t.a:16!null a:23!null + │ │ ├── cardinality: [0 - 1] + │ │ ├── multiplicity: left-rows(zero-or-one), right-rows(exactly-one) + │ │ ├── key: () + │ │ ├── fd: ()-->(14,16,23) + │ │ ├── values + │ │ │ ├── columns: a:23!null + │ │ │ ├── cardinality: [1 - 1] + │ │ │ ├── key: () + │ │ │ ├── fd: ()-->(23) + │ │ │ └── (10,) + │ │ ├── scan t@t_b_a_idx + │ │ │ ├── columns: t.k:14!null t.a:16!null + │ │ │ ├── constraint: /17/16/14 + │ │ │ │ ├── [/1/10 - /1/10/1] + │ │ │ │ └── [/1/10/3 - /1/10] + │ │ │ ├── limit: 1 + │ │ │ ├── key: () + │ │ │ └── fd: ()-->(14,16) + │ │ └── filters (true) + │ └── unique-checks-item: t(c) + │ └── project + │ ├── columns: c:44!null + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(44) + │ └── project + │ ├── columns: c:44!null + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(44) + │ └── inner-join (cross) + │ ├── columns: t.k:33!null t.c:37!null c:44!null + │ ├── cardinality: [0 - 1] + │ ├── multiplicity: left-rows(zero-or-one), right-rows(exactly-one) + │ ├── key: () + │ ├── fd: ()-->(33,37,44) + │ ├── values + │ │ ├── columns: c:44!null + │ │ ├── cardinality: [1 - 1] + │ │ ├── key: () + │ │ ├── fd: ()-->(44) + │ │ └── (20,) + │ ├── scan t@t_r_c_idx + │ │ ├── columns: t.k:33!null t.c:37!null + │ │ ├── constraint: /34/37/33 + │ │ │ ├── [/'east'/20 - /'east'/20/1] + │ │ │ ├── [/'east'/20/3 - /'east'/20] + │ │ │ ├── [/'west'/20 - /'west'/20/1] + │ │ │ └── [/'west'/20/3 - /'west'/20] + │ │ ├── limit: 1 + │ │ ├── key: () + │ │ └── fd: ()-->(33,37) + │ └── filters (true) + └── fast-path-unique-checks + ├── fast-path-unique-checks-item: t(a) + │ └── index-join t + │ ├── columns: t.k:26!null t.r:27!null t.a:28!null t.b:29 t.c:30 + │ ├── key: (26) + │ ├── fd: ()-->(28,29), (26)-->(27,30) + │ └── scan t@t_b_a_idx + │ ├── columns: t.k:26!null t.a:28!null t.b:29!null + │ ├── constraint: /29/28/26: [/1/10 - /1/10] + │ ├── key: (26) + │ └── fd: ()-->(28,29) + └── fast-path-unique-checks-item: t(c) + └── index-join t + ├── columns: t.k:45!null t.r:46!null t.a:47 t.b:48 t.c:49!null + ├── key: (45) + ├── fd: ()-->(49), (45)-->(46-48), (47)-->(48) + └── scan t@t_r_c_idx + ├── columns: t.k:45!null t.r:46!null t.c:49!null + ├── constraint: /46/49/45 + │ ├── [/'east'/20 - /'east'/20] + │ └── [/'west'/20 - /'west'/20] + ├── key: (45) + └── fd: ()-->(49), (45)-->(46) + +# Fast-path uniqueness checking is not allowed on volatile expressions, such as +# random() because the evaluation used in the check relation may differ from the +# value used in the insert row. +opt format=show-fastpathchecks expect-not=InsertFastPath +INSERT INTO t (k, r, a, c) VALUES (2, 'east', (random() * 10)::INT, 20) +---- +insert t + ├── columns: + ├── insert-mapping: + │ ├── column1:8 => t.k:1 + │ ├── column2:9 => t.r:2 + │ ├── column3:10 => t.a:3 + │ ├── b_comp:12 => t.b:4 + │ └── column4:11 => t.c:5 + ├── check columns: check1:13 + ├── input binding: &1 + ├── cardinality: [0 - 0] + ├── volatile, mutations + ├── project + │ ├── columns: check1:13!null b_comp:12 column1:8!null column2:9!null column3:10 column4:11!null + │ ├── cardinality: [1 - 1] + │ ├── volatile + │ ├── key: () + │ ├── fd: ()-->(8-13) + │ ├── values + │ │ ├── columns: column1:8!null column2:9!null column3:10 column4:11!null + │ │ ├── cardinality: [1 - 1] + │ │ ├── volatile + │ │ ├── key: () + │ │ ├── fd: ()-->(8-11) + │ │ └── (2, 'east', (random() * 10.0)::INT8, 20) + │ └── projections + │ ├── true [as=check1:13] + │ └── column3:10 % 9 [as=b_comp:12, outer=(10), immutable] + └── unique-checks + ├── unique-checks-item: t(a) + │ └── project + │ ├── columns: a:23 + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(23) + │ └── semi-join (hash) + │ ├── columns: k:21!null a:23 + │ ├── cardinality: [0 - 1] + │ ├── key: () + │ ├── fd: ()-->(21,23) + │ ├── with-scan &1 + │ │ ├── columns: k:21!null a:23 + │ │ ├── mapping: + │ │ │ ├── column1:8 => k:21 + │ │ │ └── column3:10 => a:23 + │ │ ├── cardinality: [1 - 1] + │ │ ├── key: () + │ │ └── fd: ()-->(21,23) + │ ├── scan t@t_b_a_idx + │ │ ├── columns: t.k:14!null t.a:16 + │ │ ├── key: (14) + │ │ └── fd: (14)-->(16) + │ └── filters + │ ├── a:23 = t.a:16 [outer=(16,23), constraints=(/16: (/NULL - ]; /23: (/NULL - ]), fd=(16)==(23), (23)==(16)] + │ └── k:21 != t.k:14 [outer=(14,21), constraints=(/14: (/NULL - ]; /21: (/NULL - ])] + └── unique-checks-item: t(c) + └── project + ├── columns: c:37!null + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(37) + └── semi-join (lookup t@t_r_c_idx) + ├── columns: k:33!null c:37!null + ├── lookup expression + │ └── filters + │ ├── t.r:27 IN ('east', 'west') [outer=(27), constraints=(/27: [/'east' - /'east'] [/'west' - /'west']; tight)] + │ └── c:37 = t.c:30 [outer=(30,37), constraints=(/30: (/NULL - ]; /37: (/NULL - ]), fd=(30)==(37), (37)==(30)] + ├── cardinality: [0 - 1] + ├── key: () + ├── fd: ()-->(33,37) + ├── with-scan &1 + │ ├── columns: k:33!null c:37!null + │ ├── mapping: + │ │ ├── column1:8 => k:33 + │ │ └── column4:11 => c:37 + │ ├── cardinality: [1 - 1] + │ ├── key: () + │ └── fd: ()-->(33,37) + └── filters + └── k:33 != t.k:26 [outer=(26,33), constraints=(/26: (/NULL - ]; /33: (/NULL - ])] + +exec-ddl +DROP TABLE t +---- + exec-ddl CREATE TABLE t ( k INT PRIMARY KEY, @@ -226,7 +448,7 @@ CREATE TABLE t ( # An insert to a table with multiple unique without index checks can build # fast path checks for all of them. -opt format=show-fastpathchecks +opt format=show-fastpathchecks expect=InsertFastPath INSERT INTO t (k, r, a) VALUES (2, 'east', 10) ---- insert t diff --git a/pkg/sql/opt_exec_factory.go b/pkg/sql/opt_exec_factory.go index 8c8f66cea374..1f08d58b6cc5 100644 --- a/pkg/sql/opt_exec_factory.go +++ b/pkg/sql/opt_exec_factory.go @@ -1413,6 +1413,7 @@ func (ef *execFactory) ConstructInsertFastPath( returnColOrdSet exec.TableColumnOrdinalSet, checkOrdSet exec.CheckOrdinalSet, fkChecks []exec.InsertFastPathCheck, + uniqChecks []exec.InsertFastPathCheck, autoCommit bool, ) (exec.Node, error) { // Derive insert table and column descriptors. @@ -1452,6 +1453,13 @@ func (ef *execFactory) ConstructInsertFastPath( ins.run.regionLocalInfo.setupEnforceHomeRegion(ef.planner, table, cols, ins.run.ti.ri.InsertColIDtoRowIndex) + if len(uniqChecks) > 0 { + ins.run.uniqChecks = make([]insertFastPathCheck, len(uniqChecks)) + for i := range uniqChecks { + ins.run.uniqChecks[i].InsertFastPathCheck = uniqChecks[i] + } + } + if len(fkChecks) > 0 { ins.run.fkChecks = make([]insertFastPathCheck, len(fkChecks)) for i := range fkChecks { From 2fefefcee361943ffe98e47e387710b1d7dd15f3 Mon Sep 17 00:00:00 2001 From: Mark Sirek Date: Thu, 5 Oct 2023 10:25:02 -0700 Subject: [PATCH 2/7] optbuilder: build insert fast-path uniq checks only if all checks can be built This commit avoids unnecessary processing of insert fast path uniqueness checks by modifying the optbuilder to avoid building a `FastPathUniqueChecksExpr` in the Insert expression if one or more of the required `FastPathUniqueChecksItem`s could not be built. Epic: CRDB-26290 Informs: #58047 Release note: None --- pkg/sql/opt/exec/execbuilder/mutation.go | 5 + .../opt/optbuilder/mutation_builder_unique.go | 46 +- .../optbuilder/testdata/unique-checks-insert | 1635 ++++++++--------- pkg/sql/opt/xform/coster.go | 2 +- pkg/sql/opt/xform/testdata/rules/insert | 15 +- 5 files changed, 802 insertions(+), 901 deletions(-) diff --git a/pkg/sql/opt/exec/execbuilder/mutation.go b/pkg/sql/opt/exec/execbuilder/mutation.go index 30b793512626..3caa984fb615 100644 --- a/pkg/sql/opt/exec/execbuilder/mutation.go +++ b/pkg/sql/opt/exec/execbuilder/mutation.go @@ -142,6 +142,11 @@ func (b *Builder) tryBuildFastPathInsert(ins *memo.InsertExpr) (_ execPlan, ok b if !b.allowInsertFastPath { return execPlan{}, false, nil } + // If there are unique checks required, there must be the same number of fast + // path unique checks. + if len(ins.UniqueChecks) != len(ins.FastPathUniqueChecks) { + return execPlan{}, false, nil + } insInput := ins.Input values, ok := insInput.(*memo.ValuesExpr) diff --git a/pkg/sql/opt/optbuilder/mutation_builder_unique.go b/pkg/sql/opt/optbuilder/mutation_builder_unique.go index 3ebc3bafdb63..80842979992f 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder_unique.go +++ b/pkg/sql/opt/optbuilder/mutation_builder_unique.go @@ -44,6 +44,7 @@ func (mb *mutationBuilder) buildUniqueChecksForInsert() { h := &mb.uniqueCheckHelper + buildFastPathCheck := true for i, n := 0, mb.tab.UniqueCount(); i < n; i++ { // If this constraint is already enforced by an index, we don't need to plan // a check. @@ -58,9 +59,16 @@ func (mb *mutationBuilder) buildUniqueChecksForInsert() { continue } if h.init(mb, i) { - uniqueChecksItem, fastPathUniqueChecksItem := h.buildInsertionCheck(true /* buildFastPathCheck */) + uniqueChecksItem, fastPathUniqueChecksItem := h.buildInsertionCheck(buildFastPathCheck) + if fastPathUniqueChecksItem == nil { + // If we can't build one fast path check, don't build any of them into + // the expression tree. + buildFastPathCheck = false + mb.fastPathUniqueChecks = nil + } else { + mb.fastPathUniqueChecks = append(mb.fastPathUniqueChecks, *fastPathUniqueChecksItem) + } mb.uniqueChecks = append(mb.uniqueChecks, uniqueChecksItem) - mb.fastPathUniqueChecks = append(mb.fastPathUniqueChecks, fastPathUniqueChecksItem) } } telemetry.Inc(sqltelemetry.UniqueChecksUseCounter) @@ -415,10 +423,12 @@ func (h *uniqueCheckHelper) buildFiltersForFastPathCheck( // buildInsertionCheck creates a unique check for rows which are added to a // table. The input to the insertion check will be produced from the input to // the mutation operator. If buildFastPathCheck is true, a fast-path unique -// check for the insert is also built. +// check for the insert is also built. A `UniqueChecksItem` can always be built, +// but if it is not possible to build a `FastPathUniqueChecksItem`, the second +// return value is nil. func (h *uniqueCheckHelper) buildInsertionCheck( buildFastPathCheck bool, -) (memo.UniqueChecksItem, memo.FastPathUniqueChecksItem) { +) (memo.UniqueChecksItem, *memo.FastPathUniqueChecksItem) { f := h.mb.b.factory // Build a self semi-join, with the new values on the left and the @@ -537,6 +547,14 @@ func (h *uniqueCheckHelper) buildInsertionCheck( // violation error. project := f.ConstructProject(semiJoin, nil /* projections */, keyCols.ToSet()) + uniqueChecks := f.ConstructUniqueChecksItem(project, &memo.UniqueChecksItemPrivate{ + Table: h.mb.tabID, + CheckOrdinal: h.uniqueOrdinal, + KeyCols: keyCols, + }) + if !buildFastPathCheck { + return uniqueChecks, nil + } // Build a SelectExpr which can be optimized in the explore phase and used // to build information needed to perform the fast path uniqueness check. // The goal is for the Select to be rewritten into a constrained scan on @@ -555,24 +573,16 @@ func (h *uniqueCheckHelper) buildInsertionCheck( newFilters := f.CustomFuncs().RemapScanColsInFilter(scanFilters, &scanExpr.ScanPrivate, newScanPrivate) uniqueFastPathCheck = f.ConstructSelect(newScanExpr, newFilters) } else { - uniqueFastPathCheck = f.CustomFuncs().ConstructEmptyValues(opt.ColSet{}) + // Don't build a fast-path check if we failed to create a new ScanExpr. + return uniqueChecks, nil } } else if buildFastPathCheck { - // Things blow up if a RelExpr is nil, so construct a minimal dummy relation - // that will not be used. - uniqueFastPathCheck = f.CustomFuncs().ConstructEmptyValues(opt.ColSet{}) - } - - uniqueChecks := f.ConstructUniqueChecksItem(project, &memo.UniqueChecksItemPrivate{ - Table: h.mb.tabID, - CheckOrdinal: h.uniqueOrdinal, - KeyCols: keyCols, - }) - if !buildFastPathCheck { - return uniqueChecks, memo.FastPathUniqueChecksItem{} + // Don't build a fast-path check if we failed to build a ScanExpr with + // filters on all unique check columns. + return uniqueChecks, nil } fastPathChecks := f.ConstructFastPathUniqueChecksItem(uniqueFastPathCheck, &memo.FastPathUniqueChecksItemPrivate{ReferencedTableID: h.mb.tabID, CheckOrdinal: h.uniqueOrdinal}) - return uniqueChecks, fastPathChecks + return uniqueChecks, &fastPathChecks } // buildTableScan builds a Scan of the table. The ordinals of the columns diff --git a/pkg/sql/opt/optbuilder/testdata/unique-checks-insert b/pkg/sql/opt/optbuilder/testdata/unique-checks-insert index 7e2e08f46202..8b0d7d77d7cd 100644 --- a/pkg/sql/opt/optbuilder/testdata/unique-checks-insert +++ b/pkg/sql/opt/optbuilder/testdata/unique-checks-insert @@ -26,51 +26,46 @@ insert uniq │ ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null column5:12!null │ ├── (1, 1, 1, 1, 1) │ └── (2, 2, 2, 2, 2) - ├── unique-checks - │ ├── unique-checks-item: uniq(w) - │ │ └── project - │ │ ├── columns: w:22!null - │ │ └── semi-join (hash) - │ │ ├── columns: k:20!null v:21!null w:22!null x:23!null y:24!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: k:20!null v:21!null w:22!null x:23!null y:24!null - │ │ │ └── mapping: - │ │ │ ├── column1:8 => k:20 - │ │ │ ├── column2:9 => v:21 - │ │ │ ├── column3:10 => w:22 - │ │ │ ├── column4:11 => x:23 - │ │ │ └── column5:12 => y:24 - │ │ ├── scan uniq - │ │ │ ├── columns: uniq.k:13!null uniq.v:14 uniq.w:15 uniq.x:16 uniq.y:17 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── w:22 = uniq.w:15 - │ │ └── k:20 != uniq.k:13 - │ └── unique-checks-item: uniq(x,y) - │ └── project - │ ├── columns: x:35!null y:36!null - │ └── semi-join (hash) - │ ├── columns: k:32!null v:33!null w:34!null x:35!null y:36!null - │ ├── with-scan &1 - │ │ ├── columns: k:32!null v:33!null w:34!null x:35!null y:36!null - │ │ └── mapping: - │ │ ├── column1:8 => k:32 - │ │ ├── column2:9 => v:33 - │ │ ├── column3:10 => w:34 - │ │ ├── column4:11 => x:35 - │ │ └── column5:12 => y:36 - │ ├── scan uniq - │ │ ├── columns: uniq.k:25!null uniq.v:26 uniq.w:27 uniq.x:28 uniq.y:29 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── x:35 = uniq.x:28 - │ ├── y:36 = uniq.y:29 - │ └── k:32 != uniq.k:25 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq(w) - │ └── values - └── fast-path-unique-checks-item: uniq(x,y) - └── values + └── unique-checks + ├── unique-checks-item: uniq(w) + │ └── project + │ ├── columns: w:22!null + │ └── semi-join (hash) + │ ├── columns: k:20!null v:21!null w:22!null x:23!null y:24!null + │ ├── with-scan &1 + │ │ ├── columns: k:20!null v:21!null w:22!null x:23!null y:24!null + │ │ └── mapping: + │ │ ├── column1:8 => k:20 + │ │ ├── column2:9 => v:21 + │ │ ├── column3:10 => w:22 + │ │ ├── column4:11 => x:23 + │ │ └── column5:12 => y:24 + │ ├── scan uniq + │ │ ├── columns: uniq.k:13!null uniq.v:14 uniq.w:15 uniq.x:16 uniq.y:17 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── w:22 = uniq.w:15 + │ └── k:20 != uniq.k:13 + └── unique-checks-item: uniq(x,y) + └── project + ├── columns: x:35!null y:36!null + └── semi-join (hash) + ├── columns: k:32!null v:33!null w:34!null x:35!null y:36!null + ├── with-scan &1 + │ ├── columns: k:32!null v:33!null w:34!null x:35!null y:36!null + │ └── mapping: + │ ├── column1:8 => k:32 + │ ├── column2:9 => v:33 + │ ├── column3:10 => w:34 + │ ├── column4:11 => x:35 + │ └── column5:12 => y:36 + ├── scan uniq + │ ├── columns: uniq.k:25!null uniq.v:26 uniq.w:27 uniq.x:28 uniq.y:29 + │ └── flags: disabled not visible index feature + └── filters + ├── x:35 = uniq.x:28 + ├── y:36 = uniq.y:29 + └── k:32 != uniq.k:25 # Some of the inserted values have nulls. build @@ -90,51 +85,46 @@ insert uniq │ ├── (1, 1, 1, 1, 1) │ ├── (2, 2, 2, 2, 2) │ └── (3, NULL::INT8, NULL::INT8, NULL::INT8, 3) - ├── unique-checks - │ ├── unique-checks-item: uniq(w) - │ │ └── project - │ │ ├── columns: w:22 - │ │ └── semi-join (hash) - │ │ ├── columns: k:20!null v:21 w:22 x:23 y:24!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: k:20!null v:21 w:22 x:23 y:24!null - │ │ │ └── mapping: - │ │ │ ├── column1:8 => k:20 - │ │ │ ├── column2:9 => v:21 - │ │ │ ├── column3:10 => w:22 - │ │ │ ├── column4:11 => x:23 - │ │ │ └── column5:12 => y:24 - │ │ ├── scan uniq - │ │ │ ├── columns: uniq.k:13!null uniq.v:14 uniq.w:15 uniq.x:16 uniq.y:17 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── w:22 = uniq.w:15 - │ │ └── k:20 != uniq.k:13 - │ └── unique-checks-item: uniq(x,y) - │ └── project - │ ├── columns: x:35 y:36!null - │ └── semi-join (hash) - │ ├── columns: k:32!null v:33 w:34 x:35 y:36!null - │ ├── with-scan &1 - │ │ ├── columns: k:32!null v:33 w:34 x:35 y:36!null - │ │ └── mapping: - │ │ ├── column1:8 => k:32 - │ │ ├── column2:9 => v:33 - │ │ ├── column3:10 => w:34 - │ │ ├── column4:11 => x:35 - │ │ └── column5:12 => y:36 - │ ├── scan uniq - │ │ ├── columns: uniq.k:25!null uniq.v:26 uniq.w:27 uniq.x:28 uniq.y:29 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── x:35 = uniq.x:28 - │ ├── y:36 = uniq.y:29 - │ └── k:32 != uniq.k:25 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq(w) - │ └── values - └── fast-path-unique-checks-item: uniq(x,y) - └── values + └── unique-checks + ├── unique-checks-item: uniq(w) + │ └── project + │ ├── columns: w:22 + │ └── semi-join (hash) + │ ├── columns: k:20!null v:21 w:22 x:23 y:24!null + │ ├── with-scan &1 + │ │ ├── columns: k:20!null v:21 w:22 x:23 y:24!null + │ │ └── mapping: + │ │ ├── column1:8 => k:20 + │ │ ├── column2:9 => v:21 + │ │ ├── column3:10 => w:22 + │ │ ├── column4:11 => x:23 + │ │ └── column5:12 => y:24 + │ ├── scan uniq + │ │ ├── columns: uniq.k:13!null uniq.v:14 uniq.w:15 uniq.x:16 uniq.y:17 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── w:22 = uniq.w:15 + │ └── k:20 != uniq.k:13 + └── unique-checks-item: uniq(x,y) + └── project + ├── columns: x:35 y:36!null + └── semi-join (hash) + ├── columns: k:32!null v:33 w:34 x:35 y:36!null + ├── with-scan &1 + │ ├── columns: k:32!null v:33 w:34 x:35 y:36!null + │ └── mapping: + │ ├── column1:8 => k:32 + │ ├── column2:9 => v:33 + │ ├── column3:10 => w:34 + │ ├── column4:11 => x:35 + │ └── column5:12 => y:36 + ├── scan uniq + │ ├── columns: uniq.k:25!null uniq.v:26 uniq.w:27 uniq.x:28 uniq.y:29 + │ └── flags: disabled not visible index feature + └── filters + ├── x:35 = uniq.x:28 + ├── y:36 = uniq.y:29 + └── k:32 != uniq.k:25 # No need to plan checks for w since it's always null. # NOTE: We use the norm directive here so that assignment casts are eliminated @@ -155,28 +145,25 @@ insert uniq │ ├── columns: column1:8!null column2:9!null column3:10 column4:11!null column5:12!null │ ├── (1, 1, NULL, 1, 1) │ └── (2, 2, NULL, 2, 2) - ├── unique-checks - │ └── unique-checks-item: uniq(x,y) - │ └── project - │ ├── columns: x:23!null y:24!null - │ └── semi-join (hash) - │ ├── columns: k:20!null x:23!null y:24!null - │ ├── with-scan &1 - │ │ ├── columns: k:20!null x:23!null y:24!null - │ │ └── mapping: - │ │ ├── column1:8 => k:20 - │ │ ├── column4:11 => x:23 - │ │ └── column5:12 => y:24 - │ ├── scan uniq - │ │ ├── columns: uniq.k:13!null uniq.x:16 uniq.y:17 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── x:23 = uniq.x:16 - │ ├── y:24 = uniq.y:17 - │ └── k:20 != uniq.k:13 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq(x,y) - └── values + └── unique-checks + └── unique-checks-item: uniq(x,y) + └── project + ├── columns: x:23!null y:24!null + └── semi-join (hash) + ├── columns: k:20!null x:23!null y:24!null + ├── with-scan &1 + │ ├── columns: k:20!null x:23!null y:24!null + │ └── mapping: + │ ├── column1:8 => k:20 + │ ├── column4:11 => x:23 + │ └── column5:12 => y:24 + ├── scan uniq + │ ├── columns: uniq.k:13!null uniq.x:16 uniq.y:17 + │ └── flags: disabled not visible index feature + └── filters + ├── x:23 = uniq.x:16 + ├── y:24 = uniq.y:17 + └── k:20 != uniq.k:13 # No need to plan checks for x,y since x is always null. # NOTE: We use the norm directive here so that assignment casts are eliminated @@ -197,26 +184,23 @@ insert uniq │ ├── columns: column1:8!null column2:9!null column3:10 column4:11 column5:12!null │ ├── (1, 1, 1, NULL, 1) │ └── (2, 2, NULL, NULL, 2) - ├── unique-checks - │ └── unique-checks-item: uniq(w) - │ └── project - │ ├── columns: w:22 - │ └── semi-join (hash) - │ ├── columns: k:20!null w:22 - │ ├── with-scan &1 - │ │ ├── columns: k:20!null w:22 - │ │ └── mapping: - │ │ ├── column1:8 => k:20 - │ │ └── column3:10 => w:22 - │ ├── scan uniq - │ │ ├── columns: uniq.k:13!null uniq.w:15 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── w:22 = uniq.w:15 - │ └── k:20 != uniq.k:13 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq(w) - └── values + └── unique-checks + └── unique-checks-item: uniq(w) + └── project + ├── columns: w:22 + └── semi-join (hash) + ├── columns: k:20!null w:22 + ├── with-scan &1 + │ ├── columns: k:20!null w:22 + │ └── mapping: + │ ├── column1:8 => k:20 + │ └── column3:10 => w:22 + ├── scan uniq + │ ├── columns: uniq.k:13!null uniq.w:15 + │ └── flags: disabled not visible index feature + └── filters + ├── w:22 = uniq.w:15 + └── k:20 != uniq.k:13 # No need to plan checks for x,y since y is always null. # NOTE: We use the norm directive here so that assignment casts are eliminated @@ -237,26 +221,23 @@ insert uniq │ ├── columns: column1:8!null column2:9!null column3:10!null column4:11!null column5:12 │ ├── (1, 1, 1, 1, NULL) │ └── (2, 2, 2, 2, NULL) - ├── unique-checks - │ └── unique-checks-item: uniq(w) - │ └── project - │ ├── columns: w:22!null - │ └── semi-join (hash) - │ ├── columns: k:20!null w:22!null - │ ├── with-scan &1 - │ │ ├── columns: k:20!null w:22!null - │ │ └── mapping: - │ │ ├── column1:8 => k:20 - │ │ └── column3:10 => w:22 - │ ├── scan uniq - │ │ ├── columns: uniq.k:13!null uniq.w:15 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── w:22 = uniq.w:15 - │ └── k:20 != uniq.k:13 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq(w) - └── values + └── unique-checks + └── unique-checks-item: uniq(w) + └── project + ├── columns: w:22!null + └── semi-join (hash) + ├── columns: k:20!null w:22!null + ├── with-scan &1 + │ ├── columns: k:20!null w:22!null + │ └── mapping: + │ ├── column1:8 => k:20 + │ └── column3:10 => w:22 + ├── scan uniq + │ ├── columns: uniq.k:13!null uniq.w:15 + │ └── flags: disabled not visible index feature + └── filters + ├── w:22 = uniq.w:15 + └── k:20 != uniq.k:13 # No need to plan any checks, since w, x and y are always null. # NOTE: We use the norm directive here so that assignment casts are eliminated @@ -472,30 +453,27 @@ insert uniq │ │ └── column4:11 │ └── first-agg [as=column5:12] │ └── column5:12 - ├── unique-checks - │ └── unique-checks-item: uniq(x,y) - │ └── project - │ ├── columns: x:30!null y:31!null - │ └── semi-join (hash) - │ ├── columns: k:27!null v:28!null w:29!null x:30!null y:31!null - │ ├── with-scan &1 - │ │ ├── columns: k:27!null v:28!null w:29!null x:30!null y:31!null - │ │ └── mapping: - │ │ ├── column1:8 => k:27 - │ │ ├── column2:9 => v:28 - │ │ ├── column3:10 => w:29 - │ │ ├── column4:11 => x:30 - │ │ └── column5:12 => y:31 - │ ├── scan uniq - │ │ ├── columns: uniq.k:20!null uniq.v:21 uniq.w:22 uniq.x:23 uniq.y:24 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── x:30 = uniq.x:23 - │ ├── y:31 = uniq.y:24 - │ └── k:27 != uniq.k:20 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq(x,y) - └── values + └── unique-checks + └── unique-checks-item: uniq(x,y) + └── project + ├── columns: x:30!null y:31!null + └── semi-join (hash) + ├── columns: k:27!null v:28!null w:29!null x:30!null y:31!null + ├── with-scan &1 + │ ├── columns: k:27!null v:28!null w:29!null x:30!null y:31!null + │ └── mapping: + │ ├── column1:8 => k:27 + │ ├── column2:9 => v:28 + │ ├── column3:10 => w:29 + │ ├── column4:11 => x:30 + │ └── column5:12 => y:31 + ├── scan uniq + │ ├── columns: uniq.k:20!null uniq.v:21 uniq.w:22 uniq.x:23 uniq.y:24 + │ └── flags: disabled not visible index feature + └── filters + ├── x:30 = uniq.x:23 + ├── y:31 = uniq.y:24 + └── k:27 != uniq.k:20 # On conflict clause references unique without index constraint explicitly. build @@ -580,51 +558,46 @@ insert uniq │ ├── columns: other.k:8 other.v:9 other.w:10!null other.x:11 other.y:12 │ └── scan other │ └── columns: other.k:8 other.v:9 other.w:10!null other.x:11 other.y:12 rowid:13!null other.crdb_internal_mvcc_timestamp:14 other.tableoid:15 - ├── unique-checks - │ ├── unique-checks-item: uniq(w) - │ │ └── project - │ │ ├── columns: w:25!null - │ │ └── semi-join (hash) - │ │ ├── columns: k:23 v:24 w:25!null x:26 y:27 - │ │ ├── with-scan &1 - │ │ │ ├── columns: k:23 v:24 w:25!null x:26 y:27 - │ │ │ └── mapping: - │ │ │ ├── other.k:8 => k:23 - │ │ │ ├── other.v:9 => v:24 - │ │ │ ├── other.w:10 => w:25 - │ │ │ ├── other.x:11 => x:26 - │ │ │ └── other.y:12 => y:27 - │ │ ├── scan uniq - │ │ │ ├── columns: uniq.k:16!null uniq.v:17 uniq.w:18 uniq.x:19 uniq.y:20 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── w:25 = uniq.w:18 - │ │ └── k:23 != uniq.k:16 - │ └── unique-checks-item: uniq(x,y) - │ └── project - │ ├── columns: x:38 y:39 - │ └── semi-join (hash) - │ ├── columns: k:35 v:36 w:37!null x:38 y:39 - │ ├── with-scan &1 - │ │ ├── columns: k:35 v:36 w:37!null x:38 y:39 - │ │ └── mapping: - │ │ ├── other.k:8 => k:35 - │ │ ├── other.v:9 => v:36 - │ │ ├── other.w:10 => w:37 - │ │ ├── other.x:11 => x:38 - │ │ └── other.y:12 => y:39 - │ ├── scan uniq - │ │ ├── columns: uniq.k:28!null uniq.v:29 uniq.w:30 uniq.x:31 uniq.y:32 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── x:38 = uniq.x:31 - │ ├── y:39 = uniq.y:32 - │ └── k:35 != uniq.k:28 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq(w) - │ └── values - └── fast-path-unique-checks-item: uniq(x,y) - └── values + └── unique-checks + ├── unique-checks-item: uniq(w) + │ └── project + │ ├── columns: w:25!null + │ └── semi-join (hash) + │ ├── columns: k:23 v:24 w:25!null x:26 y:27 + │ ├── with-scan &1 + │ │ ├── columns: k:23 v:24 w:25!null x:26 y:27 + │ │ └── mapping: + │ │ ├── other.k:8 => k:23 + │ │ ├── other.v:9 => v:24 + │ │ ├── other.w:10 => w:25 + │ │ ├── other.x:11 => x:26 + │ │ └── other.y:12 => y:27 + │ ├── scan uniq + │ │ ├── columns: uniq.k:16!null uniq.v:17 uniq.w:18 uniq.x:19 uniq.y:20 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── w:25 = uniq.w:18 + │ └── k:23 != uniq.k:16 + └── unique-checks-item: uniq(x,y) + └── project + ├── columns: x:38 y:39 + └── semi-join (hash) + ├── columns: k:35 v:36 w:37!null x:38 y:39 + ├── with-scan &1 + │ ├── columns: k:35 v:36 w:37!null x:38 y:39 + │ └── mapping: + │ ├── other.k:8 => k:35 + │ ├── other.v:9 => v:36 + │ ├── other.w:10 => w:37 + │ ├── other.x:11 => x:38 + │ └── other.y:12 => y:39 + ├── scan uniq + │ ├── columns: uniq.k:28!null uniq.v:29 uniq.w:30 uniq.x:31 uniq.y:32 + │ └── flags: disabled not visible index feature + └── filters + ├── x:38 = uniq.x:31 + ├── y:39 = uniq.y:32 + └── k:35 != uniq.k:28 exec-ddl CREATE TABLE uniq_overlaps_pk ( @@ -658,70 +631,63 @@ insert uniq_overlaps_pk │ ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null │ ├── (1, 1, 1, 1) │ └── (2, 2, 2, 2) - ├── unique-checks - │ ├── unique-checks-item: uniq_overlaps_pk(b,c) - │ │ └── project - │ │ ├── columns: b:18!null c:19!null - │ │ └── semi-join (hash) - │ │ ├── columns: a:17!null b:18!null c:19!null d:20!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:17!null b:18!null c:19!null d:20!null - │ │ │ └── mapping: - │ │ │ ├── column1:7 => a:17 - │ │ │ ├── column2:8 => b:18 - │ │ │ ├── column3:9 => c:19 - │ │ │ └── column4:10 => d:20 - │ │ ├── scan uniq_overlaps_pk - │ │ │ ├── columns: uniq_overlaps_pk.a:11!null uniq_overlaps_pk.b:12!null uniq_overlaps_pk.c:13 uniq_overlaps_pk.d:14 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── b:18 = uniq_overlaps_pk.b:12 - │ │ ├── c:19 = uniq_overlaps_pk.c:13 - │ │ └── a:17 != uniq_overlaps_pk.a:11 - │ ├── unique-checks-item: uniq_overlaps_pk(a) - │ │ └── project - │ │ ├── columns: a:27!null - │ │ └── semi-join (hash) - │ │ ├── columns: a:27!null b:28!null c:29!null d:30!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:27!null b:28!null c:29!null d:30!null - │ │ │ └── mapping: - │ │ │ ├── column1:7 => a:27 - │ │ │ ├── column2:8 => b:28 - │ │ │ ├── column3:9 => c:29 - │ │ │ └── column4:10 => d:30 - │ │ ├── scan uniq_overlaps_pk - │ │ │ ├── columns: uniq_overlaps_pk.a:21!null uniq_overlaps_pk.b:22!null uniq_overlaps_pk.c:23 uniq_overlaps_pk.d:24 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:27 = uniq_overlaps_pk.a:21 - │ │ └── b:28 != uniq_overlaps_pk.b:22 - │ └── unique-checks-item: uniq_overlaps_pk(c,d) - │ └── project - │ ├── columns: c:39!null d:40!null - │ └── semi-join (hash) - │ ├── columns: a:37!null b:38!null c:39!null d:40!null - │ ├── with-scan &1 - │ │ ├── columns: a:37!null b:38!null c:39!null d:40!null - │ │ └── mapping: - │ │ ├── column1:7 => a:37 - │ │ ├── column2:8 => b:38 - │ │ ├── column3:9 => c:39 - │ │ └── column4:10 => d:40 - │ ├── scan uniq_overlaps_pk - │ │ ├── columns: uniq_overlaps_pk.a:31!null uniq_overlaps_pk.b:32!null uniq_overlaps_pk.c:33 uniq_overlaps_pk.d:34 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── c:39 = uniq_overlaps_pk.c:33 - │ ├── d:40 = uniq_overlaps_pk.d:34 - │ └── (a:37 != uniq_overlaps_pk.a:31) OR (b:38 != uniq_overlaps_pk.b:32) - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq_overlaps_pk(b,c) - │ └── values - ├── fast-path-unique-checks-item: uniq_overlaps_pk(a) - │ └── values - └── fast-path-unique-checks-item: uniq_overlaps_pk(c,d) - └── values + └── unique-checks + ├── unique-checks-item: uniq_overlaps_pk(b,c) + │ └── project + │ ├── columns: b:18!null c:19!null + │ └── semi-join (hash) + │ ├── columns: a:17!null b:18!null c:19!null d:20!null + │ ├── with-scan &1 + │ │ ├── columns: a:17!null b:18!null c:19!null d:20!null + │ │ └── mapping: + │ │ ├── column1:7 => a:17 + │ │ ├── column2:8 => b:18 + │ │ ├── column3:9 => c:19 + │ │ └── column4:10 => d:20 + │ ├── scan uniq_overlaps_pk + │ │ ├── columns: uniq_overlaps_pk.a:11!null uniq_overlaps_pk.b:12!null uniq_overlaps_pk.c:13 uniq_overlaps_pk.d:14 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── b:18 = uniq_overlaps_pk.b:12 + │ ├── c:19 = uniq_overlaps_pk.c:13 + │ └── a:17 != uniq_overlaps_pk.a:11 + ├── unique-checks-item: uniq_overlaps_pk(a) + │ └── project + │ ├── columns: a:27!null + │ └── semi-join (hash) + │ ├── columns: a:27!null b:28!null c:29!null d:30!null + │ ├── with-scan &1 + │ │ ├── columns: a:27!null b:28!null c:29!null d:30!null + │ │ └── mapping: + │ │ ├── column1:7 => a:27 + │ │ ├── column2:8 => b:28 + │ │ ├── column3:9 => c:29 + │ │ └── column4:10 => d:30 + │ ├── scan uniq_overlaps_pk + │ │ ├── columns: uniq_overlaps_pk.a:21!null uniq_overlaps_pk.b:22!null uniq_overlaps_pk.c:23 uniq_overlaps_pk.d:24 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:27 = uniq_overlaps_pk.a:21 + │ └── b:28 != uniq_overlaps_pk.b:22 + └── unique-checks-item: uniq_overlaps_pk(c,d) + └── project + ├── columns: c:39!null d:40!null + └── semi-join (hash) + ├── columns: a:37!null b:38!null c:39!null d:40!null + ├── with-scan &1 + │ ├── columns: a:37!null b:38!null c:39!null d:40!null + │ └── mapping: + │ ├── column1:7 => a:37 + │ ├── column2:8 => b:38 + │ ├── column3:9 => c:39 + │ └── column4:10 => d:40 + ├── scan uniq_overlaps_pk + │ ├── columns: uniq_overlaps_pk.a:31!null uniq_overlaps_pk.b:32!null uniq_overlaps_pk.c:33 uniq_overlaps_pk.d:34 + │ └── flags: disabled not visible index feature + └── filters + ├── c:39 = uniq_overlaps_pk.c:33 + ├── d:40 = uniq_overlaps_pk.d:34 + └── (a:37 != uniq_overlaps_pk.a:31) OR (b:38 != uniq_overlaps_pk.b:32) # Insert with non-constant input. # Add inequality filters for the primary key columns that are not part of each @@ -741,70 +707,63 @@ insert uniq_overlaps_pk │ ├── columns: k:7 v:8 x:10 y:11 │ └── scan other │ └── columns: k:7 v:8 w:9!null x:10 y:11 rowid:12!null other.crdb_internal_mvcc_timestamp:13 other.tableoid:14 - ├── unique-checks - │ ├── unique-checks-item: uniq_overlaps_pk(b,c) - │ │ └── project - │ │ ├── columns: b:22 c:23 - │ │ └── semi-join (hash) - │ │ ├── columns: a:21 b:22 c:23 d:24 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:21 b:22 c:23 d:24 - │ │ │ └── mapping: - │ │ │ ├── k:7 => a:21 - │ │ │ ├── v:8 => b:22 - │ │ │ ├── x:10 => c:23 - │ │ │ └── y:11 => d:24 - │ │ ├── scan uniq_overlaps_pk - │ │ │ ├── columns: uniq_overlaps_pk.a:15!null uniq_overlaps_pk.b:16!null uniq_overlaps_pk.c:17 uniq_overlaps_pk.d:18 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── b:22 = uniq_overlaps_pk.b:16 - │ │ ├── c:23 = uniq_overlaps_pk.c:17 - │ │ └── a:21 != uniq_overlaps_pk.a:15 - │ ├── unique-checks-item: uniq_overlaps_pk(a) - │ │ └── project - │ │ ├── columns: a:31 - │ │ └── semi-join (hash) - │ │ ├── columns: a:31 b:32 c:33 d:34 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:31 b:32 c:33 d:34 - │ │ │ └── mapping: - │ │ │ ├── k:7 => a:31 - │ │ │ ├── v:8 => b:32 - │ │ │ ├── x:10 => c:33 - │ │ │ └── y:11 => d:34 - │ │ ├── scan uniq_overlaps_pk - │ │ │ ├── columns: uniq_overlaps_pk.a:25!null uniq_overlaps_pk.b:26!null uniq_overlaps_pk.c:27 uniq_overlaps_pk.d:28 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:31 = uniq_overlaps_pk.a:25 - │ │ └── b:32 != uniq_overlaps_pk.b:26 - │ └── unique-checks-item: uniq_overlaps_pk(c,d) - │ └── project - │ ├── columns: c:43 d:44 - │ └── semi-join (hash) - │ ├── columns: a:41 b:42 c:43 d:44 - │ ├── with-scan &1 - │ │ ├── columns: a:41 b:42 c:43 d:44 - │ │ └── mapping: - │ │ ├── k:7 => a:41 - │ │ ├── v:8 => b:42 - │ │ ├── x:10 => c:43 - │ │ └── y:11 => d:44 - │ ├── scan uniq_overlaps_pk - │ │ ├── columns: uniq_overlaps_pk.a:35!null uniq_overlaps_pk.b:36!null uniq_overlaps_pk.c:37 uniq_overlaps_pk.d:38 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── c:43 = uniq_overlaps_pk.c:37 - │ ├── d:44 = uniq_overlaps_pk.d:38 - │ └── (a:41 != uniq_overlaps_pk.a:35) OR (b:42 != uniq_overlaps_pk.b:36) - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq_overlaps_pk(b,c) - │ └── values - ├── fast-path-unique-checks-item: uniq_overlaps_pk(a) - │ └── values - └── fast-path-unique-checks-item: uniq_overlaps_pk(c,d) - └── values + └── unique-checks + ├── unique-checks-item: uniq_overlaps_pk(b,c) + │ └── project + │ ├── columns: b:22 c:23 + │ └── semi-join (hash) + │ ├── columns: a:21 b:22 c:23 d:24 + │ ├── with-scan &1 + │ │ ├── columns: a:21 b:22 c:23 d:24 + │ │ └── mapping: + │ │ ├── k:7 => a:21 + │ │ ├── v:8 => b:22 + │ │ ├── x:10 => c:23 + │ │ └── y:11 => d:24 + │ ├── scan uniq_overlaps_pk + │ │ ├── columns: uniq_overlaps_pk.a:15!null uniq_overlaps_pk.b:16!null uniq_overlaps_pk.c:17 uniq_overlaps_pk.d:18 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── b:22 = uniq_overlaps_pk.b:16 + │ ├── c:23 = uniq_overlaps_pk.c:17 + │ └── a:21 != uniq_overlaps_pk.a:15 + ├── unique-checks-item: uniq_overlaps_pk(a) + │ └── project + │ ├── columns: a:31 + │ └── semi-join (hash) + │ ├── columns: a:31 b:32 c:33 d:34 + │ ├── with-scan &1 + │ │ ├── columns: a:31 b:32 c:33 d:34 + │ │ └── mapping: + │ │ ├── k:7 => a:31 + │ │ ├── v:8 => b:32 + │ │ ├── x:10 => c:33 + │ │ └── y:11 => d:34 + │ ├── scan uniq_overlaps_pk + │ │ ├── columns: uniq_overlaps_pk.a:25!null uniq_overlaps_pk.b:26!null uniq_overlaps_pk.c:27 uniq_overlaps_pk.d:28 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:31 = uniq_overlaps_pk.a:25 + │ └── b:32 != uniq_overlaps_pk.b:26 + └── unique-checks-item: uniq_overlaps_pk(c,d) + └── project + ├── columns: c:43 d:44 + └── semi-join (hash) + ├── columns: a:41 b:42 c:43 d:44 + ├── with-scan &1 + │ ├── columns: a:41 b:42 c:43 d:44 + │ └── mapping: + │ ├── k:7 => a:41 + │ ├── v:8 => b:42 + │ ├── x:10 => c:43 + │ └── y:11 => d:44 + ├── scan uniq_overlaps_pk + │ ├── columns: uniq_overlaps_pk.a:35!null uniq_overlaps_pk.b:36!null uniq_overlaps_pk.c:37 uniq_overlaps_pk.d:38 + │ └── flags: disabled not visible index feature + └── filters + ├── c:43 = uniq_overlaps_pk.c:37 + ├── d:44 = uniq_overlaps_pk.d:38 + └── (a:41 != uniq_overlaps_pk.a:35) OR (b:42 != uniq_overlaps_pk.b:36) exec-ddl CREATE TABLE uniq_hidden_pk ( @@ -840,74 +799,67 @@ insert uniq_hidden_pk │ │ └── (2, 2, 2, 2) │ └── projections │ └── unique_rowid() [as=rowid_default:12] - ├── unique-checks - │ ├── unique-checks-item: uniq_hidden_pk(b,c) - │ │ └── project - │ │ ├── columns: b:21!null c:22!null - │ │ └── semi-join (hash) - │ │ ├── columns: a:20!null b:21!null c:22!null d:23!null rowid:24 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:20!null b:21!null c:22!null d:23!null rowid:24 - │ │ │ └── mapping: - │ │ │ ├── column1:8 => a:20 - │ │ │ ├── column2:9 => b:21 - │ │ │ ├── column3:10 => c:22 - │ │ │ ├── column4:11 => d:23 - │ │ │ └── rowid_default:12 => rowid:24 - │ │ ├── scan uniq_hidden_pk - │ │ │ ├── columns: uniq_hidden_pk.a:13 uniq_hidden_pk.b:14 uniq_hidden_pk.c:15 uniq_hidden_pk.d:16 uniq_hidden_pk.rowid:17!null - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── b:21 = uniq_hidden_pk.b:14 - │ │ ├── c:22 = uniq_hidden_pk.c:15 - │ │ └── rowid:24 != uniq_hidden_pk.rowid:17 - │ ├── unique-checks-item: uniq_hidden_pk(a,b,d) - │ │ └── project - │ │ ├── columns: a:32!null b:33!null d:35!null - │ │ └── semi-join (hash) - │ │ ├── columns: a:32!null b:33!null c:34!null d:35!null rowid:36 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:32!null b:33!null c:34!null d:35!null rowid:36 - │ │ │ └── mapping: - │ │ │ ├── column1:8 => a:32 - │ │ │ ├── column2:9 => b:33 - │ │ │ ├── column3:10 => c:34 - │ │ │ ├── column4:11 => d:35 - │ │ │ └── rowid_default:12 => rowid:36 - │ │ ├── scan uniq_hidden_pk - │ │ │ ├── columns: uniq_hidden_pk.a:25 uniq_hidden_pk.b:26 uniq_hidden_pk.c:27 uniq_hidden_pk.d:28 uniq_hidden_pk.rowid:29!null - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:32 = uniq_hidden_pk.a:25 - │ │ ├── b:33 = uniq_hidden_pk.b:26 - │ │ ├── d:35 = uniq_hidden_pk.d:28 - │ │ └── rowid:36 != uniq_hidden_pk.rowid:29 - │ └── unique-checks-item: uniq_hidden_pk(a) - │ └── project - │ ├── columns: a:44!null - │ └── semi-join (hash) - │ ├── columns: a:44!null b:45!null c:46!null d:47!null rowid:48 - │ ├── with-scan &1 - │ │ ├── columns: a:44!null b:45!null c:46!null d:47!null rowid:48 - │ │ └── mapping: - │ │ ├── column1:8 => a:44 - │ │ ├── column2:9 => b:45 - │ │ ├── column3:10 => c:46 - │ │ ├── column4:11 => d:47 - │ │ └── rowid_default:12 => rowid:48 - │ ├── scan uniq_hidden_pk - │ │ ├── columns: uniq_hidden_pk.a:37 uniq_hidden_pk.b:38 uniq_hidden_pk.c:39 uniq_hidden_pk.d:40 uniq_hidden_pk.rowid:41!null - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── a:44 = uniq_hidden_pk.a:37 - │ └── rowid:48 != uniq_hidden_pk.rowid:41 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq_hidden_pk(b,c) - │ └── values - ├── fast-path-unique-checks-item: uniq_hidden_pk(a,b,d) - │ └── values - └── fast-path-unique-checks-item: uniq_hidden_pk(a) - └── values + └── unique-checks + ├── unique-checks-item: uniq_hidden_pk(b,c) + │ └── project + │ ├── columns: b:21!null c:22!null + │ └── semi-join (hash) + │ ├── columns: a:20!null b:21!null c:22!null d:23!null rowid:24 + │ ├── with-scan &1 + │ │ ├── columns: a:20!null b:21!null c:22!null d:23!null rowid:24 + │ │ └── mapping: + │ │ ├── column1:8 => a:20 + │ │ ├── column2:9 => b:21 + │ │ ├── column3:10 => c:22 + │ │ ├── column4:11 => d:23 + │ │ └── rowid_default:12 => rowid:24 + │ ├── scan uniq_hidden_pk + │ │ ├── columns: uniq_hidden_pk.a:13 uniq_hidden_pk.b:14 uniq_hidden_pk.c:15 uniq_hidden_pk.d:16 uniq_hidden_pk.rowid:17!null + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── b:21 = uniq_hidden_pk.b:14 + │ ├── c:22 = uniq_hidden_pk.c:15 + │ └── rowid:24 != uniq_hidden_pk.rowid:17 + ├── unique-checks-item: uniq_hidden_pk(a,b,d) + │ └── project + │ ├── columns: a:32!null b:33!null d:35!null + │ └── semi-join (hash) + │ ├── columns: a:32!null b:33!null c:34!null d:35!null rowid:36 + │ ├── with-scan &1 + │ │ ├── columns: a:32!null b:33!null c:34!null d:35!null rowid:36 + │ │ └── mapping: + │ │ ├── column1:8 => a:32 + │ │ ├── column2:9 => b:33 + │ │ ├── column3:10 => c:34 + │ │ ├── column4:11 => d:35 + │ │ └── rowid_default:12 => rowid:36 + │ ├── scan uniq_hidden_pk + │ │ ├── columns: uniq_hidden_pk.a:25 uniq_hidden_pk.b:26 uniq_hidden_pk.c:27 uniq_hidden_pk.d:28 uniq_hidden_pk.rowid:29!null + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:32 = uniq_hidden_pk.a:25 + │ ├── b:33 = uniq_hidden_pk.b:26 + │ ├── d:35 = uniq_hidden_pk.d:28 + │ └── rowid:36 != uniq_hidden_pk.rowid:29 + └── unique-checks-item: uniq_hidden_pk(a) + └── project + ├── columns: a:44!null + └── semi-join (hash) + ├── columns: a:44!null b:45!null c:46!null d:47!null rowid:48 + ├── with-scan &1 + │ ├── columns: a:44!null b:45!null c:46!null d:47!null rowid:48 + │ └── mapping: + │ ├── column1:8 => a:44 + │ ├── column2:9 => b:45 + │ ├── column3:10 => c:46 + │ ├── column4:11 => d:47 + │ └── rowid_default:12 => rowid:48 + ├── scan uniq_hidden_pk + │ ├── columns: uniq_hidden_pk.a:37 uniq_hidden_pk.b:38 uniq_hidden_pk.c:39 uniq_hidden_pk.d:40 uniq_hidden_pk.rowid:41!null + │ └── flags: disabled not visible index feature + └── filters + ├── a:44 = uniq_hidden_pk.a:37 + └── rowid:48 != uniq_hidden_pk.rowid:41 # Insert with non-constant input. # Add inequality filters for the hidden primary key column. @@ -931,74 +883,67 @@ insert uniq_hidden_pk │ │ └── columns: k:8 v:9 w:10!null x:11 y:12 other.rowid:13!null other.crdb_internal_mvcc_timestamp:14 other.tableoid:15 │ └── projections │ └── unique_rowid() [as=rowid_default:16] - ├── unique-checks - │ ├── unique-checks-item: uniq_hidden_pk(b,c) - │ │ └── project - │ │ ├── columns: b:25 c:26 - │ │ └── semi-join (hash) - │ │ ├── columns: a:24 b:25 c:26 d:27 rowid:28 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:24 b:25 c:26 d:27 rowid:28 - │ │ │ └── mapping: - │ │ │ ├── k:8 => a:24 - │ │ │ ├── v:9 => b:25 - │ │ │ ├── x:11 => c:26 - │ │ │ ├── y:12 => d:27 - │ │ │ └── rowid_default:16 => rowid:28 - │ │ ├── scan uniq_hidden_pk - │ │ │ ├── columns: uniq_hidden_pk.a:17 uniq_hidden_pk.b:18 uniq_hidden_pk.c:19 uniq_hidden_pk.d:20 uniq_hidden_pk.rowid:21!null - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── b:25 = uniq_hidden_pk.b:18 - │ │ ├── c:26 = uniq_hidden_pk.c:19 - │ │ └── rowid:28 != uniq_hidden_pk.rowid:21 - │ ├── unique-checks-item: uniq_hidden_pk(a,b,d) - │ │ └── project - │ │ ├── columns: a:36 b:37 d:39 - │ │ └── semi-join (hash) - │ │ ├── columns: a:36 b:37 c:38 d:39 rowid:40 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:36 b:37 c:38 d:39 rowid:40 - │ │ │ └── mapping: - │ │ │ ├── k:8 => a:36 - │ │ │ ├── v:9 => b:37 - │ │ │ ├── x:11 => c:38 - │ │ │ ├── y:12 => d:39 - │ │ │ └── rowid_default:16 => rowid:40 - │ │ ├── scan uniq_hidden_pk - │ │ │ ├── columns: uniq_hidden_pk.a:29 uniq_hidden_pk.b:30 uniq_hidden_pk.c:31 uniq_hidden_pk.d:32 uniq_hidden_pk.rowid:33!null - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:36 = uniq_hidden_pk.a:29 - │ │ ├── b:37 = uniq_hidden_pk.b:30 - │ │ ├── d:39 = uniq_hidden_pk.d:32 - │ │ └── rowid:40 != uniq_hidden_pk.rowid:33 - │ └── unique-checks-item: uniq_hidden_pk(a) - │ └── project - │ ├── columns: a:48 - │ └── semi-join (hash) - │ ├── columns: a:48 b:49 c:50 d:51 rowid:52 - │ ├── with-scan &1 - │ │ ├── columns: a:48 b:49 c:50 d:51 rowid:52 - │ │ └── mapping: - │ │ ├── k:8 => a:48 - │ │ ├── v:9 => b:49 - │ │ ├── x:11 => c:50 - │ │ ├── y:12 => d:51 - │ │ └── rowid_default:16 => rowid:52 - │ ├── scan uniq_hidden_pk - │ │ ├── columns: uniq_hidden_pk.a:41 uniq_hidden_pk.b:42 uniq_hidden_pk.c:43 uniq_hidden_pk.d:44 uniq_hidden_pk.rowid:45!null - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── a:48 = uniq_hidden_pk.a:41 - │ └── rowid:52 != uniq_hidden_pk.rowid:45 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq_hidden_pk(b,c) - │ └── values - ├── fast-path-unique-checks-item: uniq_hidden_pk(a,b,d) - │ └── values - └── fast-path-unique-checks-item: uniq_hidden_pk(a) - └── values + └── unique-checks + ├── unique-checks-item: uniq_hidden_pk(b,c) + │ └── project + │ ├── columns: b:25 c:26 + │ └── semi-join (hash) + │ ├── columns: a:24 b:25 c:26 d:27 rowid:28 + │ ├── with-scan &1 + │ │ ├── columns: a:24 b:25 c:26 d:27 rowid:28 + │ │ └── mapping: + │ │ ├── k:8 => a:24 + │ │ ├── v:9 => b:25 + │ │ ├── x:11 => c:26 + │ │ ├── y:12 => d:27 + │ │ └── rowid_default:16 => rowid:28 + │ ├── scan uniq_hidden_pk + │ │ ├── columns: uniq_hidden_pk.a:17 uniq_hidden_pk.b:18 uniq_hidden_pk.c:19 uniq_hidden_pk.d:20 uniq_hidden_pk.rowid:21!null + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── b:25 = uniq_hidden_pk.b:18 + │ ├── c:26 = uniq_hidden_pk.c:19 + │ └── rowid:28 != uniq_hidden_pk.rowid:21 + ├── unique-checks-item: uniq_hidden_pk(a,b,d) + │ └── project + │ ├── columns: a:36 b:37 d:39 + │ └── semi-join (hash) + │ ├── columns: a:36 b:37 c:38 d:39 rowid:40 + │ ├── with-scan &1 + │ │ ├── columns: a:36 b:37 c:38 d:39 rowid:40 + │ │ └── mapping: + │ │ ├── k:8 => a:36 + │ │ ├── v:9 => b:37 + │ │ ├── x:11 => c:38 + │ │ ├── y:12 => d:39 + │ │ └── rowid_default:16 => rowid:40 + │ ├── scan uniq_hidden_pk + │ │ ├── columns: uniq_hidden_pk.a:29 uniq_hidden_pk.b:30 uniq_hidden_pk.c:31 uniq_hidden_pk.d:32 uniq_hidden_pk.rowid:33!null + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:36 = uniq_hidden_pk.a:29 + │ ├── b:37 = uniq_hidden_pk.b:30 + │ ├── d:39 = uniq_hidden_pk.d:32 + │ └── rowid:40 != uniq_hidden_pk.rowid:33 + └── unique-checks-item: uniq_hidden_pk(a) + └── project + ├── columns: a:48 + └── semi-join (hash) + ├── columns: a:48 b:49 c:50 d:51 rowid:52 + ├── with-scan &1 + │ ├── columns: a:48 b:49 c:50 d:51 rowid:52 + │ └── mapping: + │ ├── k:8 => a:48 + │ ├── v:9 => b:49 + │ ├── x:11 => c:50 + │ ├── y:12 => d:51 + │ └── rowid_default:16 => rowid:52 + ├── scan uniq_hidden_pk + │ ├── columns: uniq_hidden_pk.a:41 uniq_hidden_pk.b:42 uniq_hidden_pk.c:43 uniq_hidden_pk.d:44 uniq_hidden_pk.rowid:45!null + │ └── flags: disabled not visible index feature + └── filters + ├── a:48 = uniq_hidden_pk.a:41 + └── rowid:52 != uniq_hidden_pk.rowid:45 exec-ddl CREATE TABLE uniq_partial ( @@ -1024,29 +969,26 @@ insert uniq_partial │ ├── columns: column1:6!null column2:7!null column3:8!null │ ├── (1, 1, 1) │ └── (2, 2, 2) - ├── unique-checks - │ └── unique-checks-item: uniq_partial(a) - │ └── project - │ ├── columns: a:15!null - │ └── semi-join (hash) - │ ├── columns: k:14!null a:15!null b:16!null - │ ├── with-scan &1 - │ │ ├── columns: k:14!null a:15!null b:16!null - │ │ └── mapping: - │ │ ├── column1:6 => k:14 - │ │ ├── column2:7 => a:15 - │ │ └── column3:8 => b:16 - │ ├── scan uniq_partial - │ │ ├── columns: uniq_partial.k:9!null uniq_partial.a:10 uniq_partial.b:11 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── a:15 = uniq_partial.a:10 - │ ├── b:16 > 0 - │ ├── uniq_partial.b:11 > 0 - │ └── k:14 != uniq_partial.k:9 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_partial(a) - └── values + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── project + ├── columns: a:15!null + └── semi-join (hash) + ├── columns: k:14!null a:15!null b:16!null + ├── with-scan &1 + │ ├── columns: k:14!null a:15!null b:16!null + │ └── mapping: + │ ├── column1:6 => k:14 + │ ├── column2:7 => a:15 + │ └── column3:8 => b:16 + ├── scan uniq_partial + │ ├── columns: uniq_partial.k:9!null uniq_partial.a:10 uniq_partial.b:11 + │ └── flags: disabled not visible index feature + └── filters + ├── a:15 = uniq_partial.a:10 + ├── b:16 > 0 + ├── uniq_partial.b:11 > 0 + └── k:14 != uniq_partial.k:9 # Some of the inserted values have nulls. build @@ -1064,29 +1006,26 @@ insert uniq_partial │ ├── (1, 1, 1) │ ├── (2, 2, 2) │ └── (3, NULL::INT8, 3) - ├── unique-checks - │ └── unique-checks-item: uniq_partial(a) - │ └── project - │ ├── columns: a:15 - │ └── semi-join (hash) - │ ├── columns: k:14!null a:15 b:16!null - │ ├── with-scan &1 - │ │ ├── columns: k:14!null a:15 b:16!null - │ │ └── mapping: - │ │ ├── column1:6 => k:14 - │ │ ├── column2:7 => a:15 - │ │ └── column3:8 => b:16 - │ ├── scan uniq_partial - │ │ ├── columns: uniq_partial.k:9!null uniq_partial.a:10 uniq_partial.b:11 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── a:15 = uniq_partial.a:10 - │ ├── b:16 > 0 - │ ├── uniq_partial.b:11 > 0 - │ └── k:14 != uniq_partial.k:9 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_partial(a) - └── values + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── project + ├── columns: a:15 + └── semi-join (hash) + ├── columns: k:14!null a:15 b:16!null + ├── with-scan &1 + │ ├── columns: k:14!null a:15 b:16!null + │ └── mapping: + │ ├── column1:6 => k:14 + │ ├── column2:7 => a:15 + │ └── column3:8 => b:16 + ├── scan uniq_partial + │ ├── columns: uniq_partial.k:9!null uniq_partial.a:10 uniq_partial.b:11 + │ └── flags: disabled not visible index feature + └── filters + ├── a:15 = uniq_partial.a:10 + ├── b:16 > 0 + ├── uniq_partial.b:11 > 0 + └── k:14 != uniq_partial.k:9 # No need to plan checks for a since it's always null. # NOTE: We use the norm directive here so that assignment casts are eliminated @@ -1234,29 +1173,26 @@ insert uniq_partial │ ├── columns: other.k:6 v:7 w:8!null │ └── scan other │ └── columns: other.k:6 v:7 w:8!null x:9 y:10 rowid:11!null other.crdb_internal_mvcc_timestamp:12 other.tableoid:13 - ├── unique-checks - │ └── unique-checks-item: uniq_partial(a) - │ └── project - │ ├── columns: a:20 - │ └── semi-join (hash) - │ ├── columns: k:19 a:20 b:21!null - │ ├── with-scan &1 - │ │ ├── columns: k:19 a:20 b:21!null - │ │ └── mapping: - │ │ ├── other.k:6 => k:19 - │ │ ├── v:7 => a:20 - │ │ └── w:8 => b:21 - │ ├── scan uniq_partial - │ │ ├── columns: uniq_partial.k:14!null uniq_partial.a:15 uniq_partial.b:16 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── a:20 = uniq_partial.a:15 - │ ├── b:21 > 0 - │ ├── uniq_partial.b:16 > 0 - │ └── k:19 != uniq_partial.k:14 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_partial(a) - └── values + └── unique-checks + └── unique-checks-item: uniq_partial(a) + └── project + ├── columns: a:20 + └── semi-join (hash) + ├── columns: k:19 a:20 b:21!null + ├── with-scan &1 + │ ├── columns: k:19 a:20 b:21!null + │ └── mapping: + │ ├── other.k:6 => k:19 + │ ├── v:7 => a:20 + │ └── w:8 => b:21 + ├── scan uniq_partial + │ ├── columns: uniq_partial.k:14!null uniq_partial.a:15 uniq_partial.b:16 + │ └── flags: disabled not visible index feature + └── filters + ├── a:20 = uniq_partial.a:15 + ├── b:21 > 0 + ├── uniq_partial.b:16 > 0 + └── k:19 != uniq_partial.k:14 exec-ddl CREATE TABLE uniq_partial_overlaps_pk ( @@ -1291,75 +1227,68 @@ insert uniq_partial_overlaps_pk │ ├── columns: column1:7!null column2:8!null column3:9!null column4:10!null │ ├── (1, 1, 1, 1) │ └── (2, 2, 2, 2) - ├── unique-checks - │ ├── unique-checks-item: uniq_partial_overlaps_pk(c) - │ │ └── project - │ │ ├── columns: c:19!null - │ │ └── semi-join (hash) - │ │ ├── columns: a:17!null b:18!null c:19!null d:20!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:17!null b:18!null c:19!null d:20!null - │ │ │ └── mapping: - │ │ │ ├── column1:7 => a:17 - │ │ │ ├── column2:8 => b:18 - │ │ │ ├── column3:9 => c:19 - │ │ │ └── column4:10 => d:20 - │ │ ├── scan uniq_partial_overlaps_pk - │ │ │ ├── columns: uniq_partial_overlaps_pk.a:11!null uniq_partial_overlaps_pk.b:12!null uniq_partial_overlaps_pk.c:13 uniq_partial_overlaps_pk.d:14 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── c:19 = uniq_partial_overlaps_pk.c:13 - │ │ ├── d:20 > 0 - │ │ ├── uniq_partial_overlaps_pk.d:14 > 0 - │ │ └── (a:17 != uniq_partial_overlaps_pk.a:11) OR (b:18 != uniq_partial_overlaps_pk.b:12) - │ ├── unique-checks-item: uniq_partial_overlaps_pk(a) - │ │ └── project - │ │ ├── columns: a:27!null - │ │ └── semi-join (hash) - │ │ ├── columns: a:27!null b:28!null c:29!null d:30!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:27!null b:28!null c:29!null d:30!null - │ │ │ └── mapping: - │ │ │ ├── column1:7 => a:27 - │ │ │ ├── column2:8 => b:28 - │ │ │ ├── column3:9 => c:29 - │ │ │ └── column4:10 => d:30 - │ │ ├── scan uniq_partial_overlaps_pk - │ │ │ ├── columns: uniq_partial_overlaps_pk.a:21!null uniq_partial_overlaps_pk.b:22!null uniq_partial_overlaps_pk.c:23 uniq_partial_overlaps_pk.d:24 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:27 = uniq_partial_overlaps_pk.a:21 - │ │ ├── d:30 > 0 - │ │ ├── uniq_partial_overlaps_pk.d:24 > 0 - │ │ └── b:28 != uniq_partial_overlaps_pk.b:22 - │ └── unique-checks-item: uniq_partial_overlaps_pk(b,c) - │ └── project - │ ├── columns: b:38!null c:39!null - │ └── semi-join (hash) - │ ├── columns: a:37!null b:38!null c:39!null d:40!null - │ ├── with-scan &1 - │ │ ├── columns: a:37!null b:38!null c:39!null d:40!null - │ │ └── mapping: - │ │ ├── column1:7 => a:37 - │ │ ├── column2:8 => b:38 - │ │ ├── column3:9 => c:39 - │ │ └── column4:10 => d:40 - │ ├── scan uniq_partial_overlaps_pk - │ │ ├── columns: uniq_partial_overlaps_pk.a:31!null uniq_partial_overlaps_pk.b:32!null uniq_partial_overlaps_pk.c:33 uniq_partial_overlaps_pk.d:34 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── b:38 = uniq_partial_overlaps_pk.b:32 - │ ├── c:39 = uniq_partial_overlaps_pk.c:33 - │ ├── d:40 > 0 - │ ├── uniq_partial_overlaps_pk.d:34 > 0 - │ └── a:37 != uniq_partial_overlaps_pk.a:31 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq_partial_overlaps_pk(c) - │ └── values - ├── fast-path-unique-checks-item: uniq_partial_overlaps_pk(a) - │ └── values - └── fast-path-unique-checks-item: uniq_partial_overlaps_pk(b,c) - └── values + └── unique-checks + ├── unique-checks-item: uniq_partial_overlaps_pk(c) + │ └── project + │ ├── columns: c:19!null + │ └── semi-join (hash) + │ ├── columns: a:17!null b:18!null c:19!null d:20!null + │ ├── with-scan &1 + │ │ ├── columns: a:17!null b:18!null c:19!null d:20!null + │ │ └── mapping: + │ │ ├── column1:7 => a:17 + │ │ ├── column2:8 => b:18 + │ │ ├── column3:9 => c:19 + │ │ └── column4:10 => d:20 + │ ├── scan uniq_partial_overlaps_pk + │ │ ├── columns: uniq_partial_overlaps_pk.a:11!null uniq_partial_overlaps_pk.b:12!null uniq_partial_overlaps_pk.c:13 uniq_partial_overlaps_pk.d:14 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── c:19 = uniq_partial_overlaps_pk.c:13 + │ ├── d:20 > 0 + │ ├── uniq_partial_overlaps_pk.d:14 > 0 + │ └── (a:17 != uniq_partial_overlaps_pk.a:11) OR (b:18 != uniq_partial_overlaps_pk.b:12) + ├── unique-checks-item: uniq_partial_overlaps_pk(a) + │ └── project + │ ├── columns: a:27!null + │ └── semi-join (hash) + │ ├── columns: a:27!null b:28!null c:29!null d:30!null + │ ├── with-scan &1 + │ │ ├── columns: a:27!null b:28!null c:29!null d:30!null + │ │ └── mapping: + │ │ ├── column1:7 => a:27 + │ │ ├── column2:8 => b:28 + │ │ ├── column3:9 => c:29 + │ │ └── column4:10 => d:30 + │ ├── scan uniq_partial_overlaps_pk + │ │ ├── columns: uniq_partial_overlaps_pk.a:21!null uniq_partial_overlaps_pk.b:22!null uniq_partial_overlaps_pk.c:23 uniq_partial_overlaps_pk.d:24 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:27 = uniq_partial_overlaps_pk.a:21 + │ ├── d:30 > 0 + │ ├── uniq_partial_overlaps_pk.d:24 > 0 + │ └── b:28 != uniq_partial_overlaps_pk.b:22 + └── unique-checks-item: uniq_partial_overlaps_pk(b,c) + └── project + ├── columns: b:38!null c:39!null + └── semi-join (hash) + ├── columns: a:37!null b:38!null c:39!null d:40!null + ├── with-scan &1 + │ ├── columns: a:37!null b:38!null c:39!null d:40!null + │ └── mapping: + │ ├── column1:7 => a:37 + │ ├── column2:8 => b:38 + │ ├── column3:9 => c:39 + │ └── column4:10 => d:40 + ├── scan uniq_partial_overlaps_pk + │ ├── columns: uniq_partial_overlaps_pk.a:31!null uniq_partial_overlaps_pk.b:32!null uniq_partial_overlaps_pk.c:33 uniq_partial_overlaps_pk.d:34 + │ └── flags: disabled not visible index feature + └── filters + ├── b:38 = uniq_partial_overlaps_pk.b:32 + ├── c:39 = uniq_partial_overlaps_pk.c:33 + ├── d:40 > 0 + ├── uniq_partial_overlaps_pk.d:34 > 0 + └── a:37 != uniq_partial_overlaps_pk.a:31 # Insert with non-constant input. # Do not build uniqueness checks when the primary key columns are a subset of @@ -1379,75 +1308,68 @@ insert uniq_partial_overlaps_pk │ ├── columns: k:7 v:8 x:10 y:11 │ └── scan other │ └── columns: k:7 v:8 w:9!null x:10 y:11 rowid:12!null other.crdb_internal_mvcc_timestamp:13 other.tableoid:14 - ├── unique-checks - │ ├── unique-checks-item: uniq_partial_overlaps_pk(c) - │ │ └── project - │ │ ├── columns: c:23 - │ │ └── semi-join (hash) - │ │ ├── columns: a:21 b:22 c:23 d:24 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:21 b:22 c:23 d:24 - │ │ │ └── mapping: - │ │ │ ├── k:7 => a:21 - │ │ │ ├── v:8 => b:22 - │ │ │ ├── x:10 => c:23 - │ │ │ └── y:11 => d:24 - │ │ ├── scan uniq_partial_overlaps_pk - │ │ │ ├── columns: uniq_partial_overlaps_pk.a:15!null uniq_partial_overlaps_pk.b:16!null uniq_partial_overlaps_pk.c:17 uniq_partial_overlaps_pk.d:18 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── c:23 = uniq_partial_overlaps_pk.c:17 - │ │ ├── d:24 > 0 - │ │ ├── uniq_partial_overlaps_pk.d:18 > 0 - │ │ └── (a:21 != uniq_partial_overlaps_pk.a:15) OR (b:22 != uniq_partial_overlaps_pk.b:16) - │ ├── unique-checks-item: uniq_partial_overlaps_pk(a) - │ │ └── project - │ │ ├── columns: a:31 - │ │ └── semi-join (hash) - │ │ ├── columns: a:31 b:32 c:33 d:34 - │ │ ├── with-scan &1 - │ │ │ ├── columns: a:31 b:32 c:33 d:34 - │ │ │ └── mapping: - │ │ │ ├── k:7 => a:31 - │ │ │ ├── v:8 => b:32 - │ │ │ ├── x:10 => c:33 - │ │ │ └── y:11 => d:34 - │ │ ├── scan uniq_partial_overlaps_pk - │ │ │ ├── columns: uniq_partial_overlaps_pk.a:25!null uniq_partial_overlaps_pk.b:26!null uniq_partial_overlaps_pk.c:27 uniq_partial_overlaps_pk.d:28 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:31 = uniq_partial_overlaps_pk.a:25 - │ │ ├── d:34 > 0 - │ │ ├── uniq_partial_overlaps_pk.d:28 > 0 - │ │ └── b:32 != uniq_partial_overlaps_pk.b:26 - │ └── unique-checks-item: uniq_partial_overlaps_pk(b,c) - │ └── project - │ ├── columns: b:42 c:43 - │ └── semi-join (hash) - │ ├── columns: a:41 b:42 c:43 d:44 - │ ├── with-scan &1 - │ │ ├── columns: a:41 b:42 c:43 d:44 - │ │ └── mapping: - │ │ ├── k:7 => a:41 - │ │ ├── v:8 => b:42 - │ │ ├── x:10 => c:43 - │ │ └── y:11 => d:44 - │ ├── scan uniq_partial_overlaps_pk - │ │ ├── columns: uniq_partial_overlaps_pk.a:35!null uniq_partial_overlaps_pk.b:36!null uniq_partial_overlaps_pk.c:37 uniq_partial_overlaps_pk.d:38 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── b:42 = uniq_partial_overlaps_pk.b:36 - │ ├── c:43 = uniq_partial_overlaps_pk.c:37 - │ ├── d:44 > 0 - │ ├── uniq_partial_overlaps_pk.d:38 > 0 - │ └── a:41 != uniq_partial_overlaps_pk.a:35 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: uniq_partial_overlaps_pk(c) - │ └── values - ├── fast-path-unique-checks-item: uniq_partial_overlaps_pk(a) - │ └── values - └── fast-path-unique-checks-item: uniq_partial_overlaps_pk(b,c) - └── values + └── unique-checks + ├── unique-checks-item: uniq_partial_overlaps_pk(c) + │ └── project + │ ├── columns: c:23 + │ └── semi-join (hash) + │ ├── columns: a:21 b:22 c:23 d:24 + │ ├── with-scan &1 + │ │ ├── columns: a:21 b:22 c:23 d:24 + │ │ └── mapping: + │ │ ├── k:7 => a:21 + │ │ ├── v:8 => b:22 + │ │ ├── x:10 => c:23 + │ │ └── y:11 => d:24 + │ ├── scan uniq_partial_overlaps_pk + │ │ ├── columns: uniq_partial_overlaps_pk.a:15!null uniq_partial_overlaps_pk.b:16!null uniq_partial_overlaps_pk.c:17 uniq_partial_overlaps_pk.d:18 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── c:23 = uniq_partial_overlaps_pk.c:17 + │ ├── d:24 > 0 + │ ├── uniq_partial_overlaps_pk.d:18 > 0 + │ └── (a:21 != uniq_partial_overlaps_pk.a:15) OR (b:22 != uniq_partial_overlaps_pk.b:16) + ├── unique-checks-item: uniq_partial_overlaps_pk(a) + │ └── project + │ ├── columns: a:31 + │ └── semi-join (hash) + │ ├── columns: a:31 b:32 c:33 d:34 + │ ├── with-scan &1 + │ │ ├── columns: a:31 b:32 c:33 d:34 + │ │ └── mapping: + │ │ ├── k:7 => a:31 + │ │ ├── v:8 => b:32 + │ │ ├── x:10 => c:33 + │ │ └── y:11 => d:34 + │ ├── scan uniq_partial_overlaps_pk + │ │ ├── columns: uniq_partial_overlaps_pk.a:25!null uniq_partial_overlaps_pk.b:26!null uniq_partial_overlaps_pk.c:27 uniq_partial_overlaps_pk.d:28 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:31 = uniq_partial_overlaps_pk.a:25 + │ ├── d:34 > 0 + │ ├── uniq_partial_overlaps_pk.d:28 > 0 + │ └── b:32 != uniq_partial_overlaps_pk.b:26 + └── unique-checks-item: uniq_partial_overlaps_pk(b,c) + └── project + ├── columns: b:42 c:43 + └── semi-join (hash) + ├── columns: a:41 b:42 c:43 d:44 + ├── with-scan &1 + │ ├── columns: a:41 b:42 c:43 d:44 + │ └── mapping: + │ ├── k:7 => a:41 + │ ├── v:8 => b:42 + │ ├── x:10 => c:43 + │ └── y:11 => d:44 + ├── scan uniq_partial_overlaps_pk + │ ├── columns: uniq_partial_overlaps_pk.a:35!null uniq_partial_overlaps_pk.b:36!null uniq_partial_overlaps_pk.c:37 uniq_partial_overlaps_pk.d:38 + │ └── flags: disabled not visible index feature + └── filters + ├── b:42 = uniq_partial_overlaps_pk.b:36 + ├── c:43 = uniq_partial_overlaps_pk.c:37 + ├── d:44 > 0 + ├── uniq_partial_overlaps_pk.d:38 > 0 + └── a:41 != uniq_partial_overlaps_pk.a:35 exec-ddl CREATE TABLE uniq_partial_hidden_pk ( @@ -1480,30 +1402,27 @@ insert uniq_partial_hidden_pk │ └── projections │ ├── NULL::INT8 [as=c_default:9] │ └── unique_rowid() [as=rowid_default:10] - ├── unique-checks - │ └── unique-checks-item: uniq_partial_hidden_pk(b) - │ └── project - │ ├── columns: b:18!null - │ └── semi-join (hash) - │ ├── columns: a:17!null b:18!null c:19 rowid:20 - │ ├── with-scan &1 - │ │ ├── columns: a:17!null b:18!null c:19 rowid:20 - │ │ └── mapping: - │ │ ├── column1:7 => a:17 - │ │ ├── column2:8 => b:18 - │ │ ├── c_default:9 => c:19 - │ │ └── rowid_default:10 => rowid:20 - │ ├── scan uniq_partial_hidden_pk - │ │ ├── columns: uniq_partial_hidden_pk.a:11 uniq_partial_hidden_pk.b:12 uniq_partial_hidden_pk.c:13 uniq_partial_hidden_pk.rowid:14!null - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── b:18 = uniq_partial_hidden_pk.b:12 - │ ├── c:19 > 0 - │ ├── uniq_partial_hidden_pk.c:13 > 0 - │ └── rowid:20 != uniq_partial_hidden_pk.rowid:14 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_partial_hidden_pk(b) - └── values + └── unique-checks + └── unique-checks-item: uniq_partial_hidden_pk(b) + └── project + ├── columns: b:18!null + └── semi-join (hash) + ├── columns: a:17!null b:18!null c:19 rowid:20 + ├── with-scan &1 + │ ├── columns: a:17!null b:18!null c:19 rowid:20 + │ └── mapping: + │ ├── column1:7 => a:17 + │ ├── column2:8 => b:18 + │ ├── c_default:9 => c:19 + │ └── rowid_default:10 => rowid:20 + ├── scan uniq_partial_hidden_pk + │ ├── columns: uniq_partial_hidden_pk.a:11 uniq_partial_hidden_pk.b:12 uniq_partial_hidden_pk.c:13 uniq_partial_hidden_pk.rowid:14!null + │ └── flags: disabled not visible index feature + └── filters + ├── b:18 = uniq_partial_hidden_pk.b:12 + ├── c:19 > 0 + ├── uniq_partial_hidden_pk.c:13 > 0 + └── rowid:20 != uniq_partial_hidden_pk.rowid:14 # Add inequality filters for the hidden primary key column. build @@ -1526,30 +1445,27 @@ insert uniq_partial_hidden_pk │ └── projections │ ├── NULL::INT8 [as=c_default:15] │ └── unique_rowid() [as=rowid_default:16] - ├── unique-checks - │ └── unique-checks-item: uniq_partial_hidden_pk(b) - │ └── project - │ ├── columns: b:24 - │ └── semi-join (hash) - │ ├── columns: a:23 b:24 c:25 rowid:26 - │ ├── with-scan &1 - │ │ ├── columns: a:23 b:24 c:25 rowid:26 - │ │ └── mapping: - │ │ ├── k:7 => a:23 - │ │ ├── v:8 => b:24 - │ │ ├── c_default:15 => c:25 - │ │ └── rowid_default:16 => rowid:26 - │ ├── scan uniq_partial_hidden_pk - │ │ ├── columns: uniq_partial_hidden_pk.a:17 uniq_partial_hidden_pk.b:18 uniq_partial_hidden_pk.c:19 uniq_partial_hidden_pk.rowid:20!null - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── b:24 = uniq_partial_hidden_pk.b:18 - │ ├── c:25 > 0 - │ ├── uniq_partial_hidden_pk.c:19 > 0 - │ └── rowid:26 != uniq_partial_hidden_pk.rowid:20 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_partial_hidden_pk(b) - └── values + └── unique-checks + └── unique-checks-item: uniq_partial_hidden_pk(b) + └── project + ├── columns: b:24 + └── semi-join (hash) + ├── columns: a:23 b:24 c:25 rowid:26 + ├── with-scan &1 + │ ├── columns: a:23 b:24 c:25 rowid:26 + │ └── mapping: + │ ├── k:7 => a:23 + │ ├── v:8 => b:24 + │ ├── c_default:15 => c:25 + │ └── rowid_default:16 => rowid:26 + ├── scan uniq_partial_hidden_pk + │ ├── columns: uniq_partial_hidden_pk.a:17 uniq_partial_hidden_pk.b:18 uniq_partial_hidden_pk.c:19 uniq_partial_hidden_pk.rowid:20!null + │ └── flags: disabled not visible index feature + └── filters + ├── b:24 = uniq_partial_hidden_pk.b:18 + ├── c:25 > 0 + ├── uniq_partial_hidden_pk.c:19 > 0 + └── rowid:26 != uniq_partial_hidden_pk.rowid:20 exec-ddl CREATE TABLE uniq_partial_constraint_and_index ( @@ -1602,13 +1518,10 @@ insert uniq_partial_constraint_and_index │ │ └── filters (true) │ └── projections │ └── true [as=partial_index_put1:15] - ├── unique-checks - │ └── unique-checks-item: uniq_partial_constraint_and_index(a) - │ └── values - │ └── columns: a:22!null - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_partial_constraint_and_index(a) + └── unique-checks + └── unique-checks-item: uniq_partial_constraint_and_index(a) └── values + └── columns: a:22!null exec-ddl CREATE TABLE uniq_constraint_and_partial_index ( @@ -1791,44 +1704,41 @@ insert uniq_computed_pk │ └── projections │ ├── CASE WHEN column1:10 < 0 THEN 'foo' ELSE 'bar' END [as=c_i_expr_comp:13] │ └── column3:12::STRING [as=c_d_expr_comp:14] - ├── unique-checks - │ └── unique-checks-item: uniq_computed_pk(d) - │ └── project - │ ├── columns: d:44!null - │ └── semi-join (hash) - │ ├── columns: i:42!null s:43!null d:44!null c_i_expr:45!null c_s:46!null c_d:47!null c_d_expr:48!null - │ ├── with-scan &1 - │ │ ├── columns: i:42!null s:43!null d:44!null c_i_expr:45!null c_s:46!null c_d:47!null c_d_expr:48!null - │ │ └── mapping: - │ │ ├── column1:10 => i:42 - │ │ ├── column2:11 => s:43 - │ │ ├── column3:12 => d:44 - │ │ ├── c_i_expr_comp:13 => c_i_expr:45 - │ │ ├── column2:11 => c_s:46 - │ │ ├── column3:12 => c_d:47 - │ │ └── c_d_expr_comp:14 => c_d_expr:48 - │ ├── project - │ │ ├── columns: uniq_computed_pk.c_s:37 uniq_computed_pk.i:33!null uniq_computed_pk.s:34 uniq_computed_pk.d:35 uniq_computed_pk.c_i_expr:36!null uniq_computed_pk.c_d:38 uniq_computed_pk.c_d_expr:39 - │ │ ├── scan uniq_computed_pk - │ │ │ ├── columns: uniq_computed_pk.i:33!null uniq_computed_pk.s:34 uniq_computed_pk.d:35 uniq_computed_pk.c_i_expr:36!null uniq_computed_pk.c_d:38 uniq_computed_pk.c_d_expr:39 - │ │ │ ├── computed column expressions - │ │ │ │ ├── uniq_computed_pk.c_i_expr:36 - │ │ │ │ │ └── CASE WHEN uniq_computed_pk.i:33 < 0 THEN 'foo' ELSE 'bar' END - │ │ │ │ ├── uniq_computed_pk.c_s:37 - │ │ │ │ │ └── uniq_computed_pk.s:34 - │ │ │ │ ├── uniq_computed_pk.c_d:38 - │ │ │ │ │ └── uniq_computed_pk.d:35 - │ │ │ │ └── uniq_computed_pk.c_d_expr:39 - │ │ │ │ └── uniq_computed_pk.d:35::STRING - │ │ │ └── flags: disabled not visible index feature - │ │ └── projections - │ │ └── uniq_computed_pk.s:34 [as=uniq_computed_pk.c_s:37] - │ └── filters - │ ├── d:44 = uniq_computed_pk.d:35 - │ └── (i:42 != uniq_computed_pk.i:33) OR (c_i_expr:45 != uniq_computed_pk.c_i_expr:36) - └── fast-path-unique-checks - └── fast-path-unique-checks-item: uniq_computed_pk(d) - └── values + └── unique-checks + └── unique-checks-item: uniq_computed_pk(d) + └── project + ├── columns: d:44!null + └── semi-join (hash) + ├── columns: i:42!null s:43!null d:44!null c_i_expr:45!null c_s:46!null c_d:47!null c_d_expr:48!null + ├── with-scan &1 + │ ├── columns: i:42!null s:43!null d:44!null c_i_expr:45!null c_s:46!null c_d:47!null c_d_expr:48!null + │ └── mapping: + │ ├── column1:10 => i:42 + │ ├── column2:11 => s:43 + │ ├── column3:12 => d:44 + │ ├── c_i_expr_comp:13 => c_i_expr:45 + │ ├── column2:11 => c_s:46 + │ ├── column3:12 => c_d:47 + │ └── c_d_expr_comp:14 => c_d_expr:48 + ├── project + │ ├── columns: uniq_computed_pk.c_s:37 uniq_computed_pk.i:33!null uniq_computed_pk.s:34 uniq_computed_pk.d:35 uniq_computed_pk.c_i_expr:36!null uniq_computed_pk.c_d:38 uniq_computed_pk.c_d_expr:39 + │ ├── scan uniq_computed_pk + │ │ ├── columns: uniq_computed_pk.i:33!null uniq_computed_pk.s:34 uniq_computed_pk.d:35 uniq_computed_pk.c_i_expr:36!null uniq_computed_pk.c_d:38 uniq_computed_pk.c_d_expr:39 + │ │ ├── computed column expressions + │ │ │ ├── uniq_computed_pk.c_i_expr:36 + │ │ │ │ └── CASE WHEN uniq_computed_pk.i:33 < 0 THEN 'foo' ELSE 'bar' END + │ │ │ ├── uniq_computed_pk.c_s:37 + │ │ │ │ └── uniq_computed_pk.s:34 + │ │ │ ├── uniq_computed_pk.c_d:38 + │ │ │ │ └── uniq_computed_pk.d:35 + │ │ │ └── uniq_computed_pk.c_d_expr:39 + │ │ │ └── uniq_computed_pk.d:35::STRING + │ │ └── flags: disabled not visible index feature + │ └── projections + │ └── uniq_computed_pk.s:34 [as=uniq_computed_pk.c_s:37] + └── filters + ├── d:44 = uniq_computed_pk.d:35 + └── (i:42 != uniq_computed_pk.i:33) OR (c_i_expr:45 != uniq_computed_pk.c_i_expr:36) exec-ddl CREATE TABLE uniq_default ( @@ -1920,31 +1830,28 @@ insert t │ │ └── column2:8 IN ('east', 'west') [as=check1:11] │ └── projections │ └── column1:7 != 1 [as=partial_index_put1:12] - ├── unique-checks - │ └── unique-checks-item: t(a) - │ └── project - │ ├── columns: a:21!null - │ └── semi-join (hash) - │ ├── columns: k:19!null r:20!null a:21!null b:22!null - │ ├── values - │ │ ├── columns: k:19!null r:20!null a:21!null b:22!null - │ │ └── (1, 'east', 10, 100) - │ ├── scan t - │ │ ├── columns: t.k:13!null t.r:14!null t.a:15 t.b:16 - │ │ ├── check constraint expressions - │ │ │ └── t.r:14 IN ('east', 'west') - │ │ ├── partial index predicates - │ │ │ └── t_r_a_idx: filters - │ │ │ └── t.k:13 != 1 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── a:21 = t.a:15 - │ ├── k:19 != 1 - │ ├── t.k:13 != 1 - │ └── k:19 != t.k:13 - └── fast-path-unique-checks - └── fast-path-unique-checks-item: t(a) - └── values + └── unique-checks + └── unique-checks-item: t(a) + └── project + ├── columns: a:21!null + └── semi-join (hash) + ├── columns: k:19!null r:20!null a:21!null b:22!null + ├── values + │ ├── columns: k:19!null r:20!null a:21!null b:22!null + │ └── (1, 'east', 10, 100) + ├── scan t + │ ├── columns: t.k:13!null t.r:14!null t.a:15 t.b:16 + │ ├── check constraint expressions + │ │ └── t.r:14 IN ('east', 'west') + │ ├── partial index predicates + │ │ └── t_r_a_idx: filters + │ │ └── t.k:13 != 1 + │ └── flags: disabled not visible index feature + └── filters + ├── a:21 = t.a:15 + ├── k:19 != 1 + ├── t.k:13 != 1 + └── k:19 != t.k:13 exec-ddl DROP TABLE t @@ -2070,69 +1977,53 @@ insert t │ │ └── column1:7 % 9 [as=b_comp:10] │ └── projections │ └── column2:8 IN ('east', 'west') [as=check1:11] - ├── unique-checks - │ ├── unique-checks-item: t(a) - │ │ └── project - │ │ ├── columns: a:20!null - │ │ └── semi-join (hash) - │ │ ├── columns: k:18!null r:19!null a:20!null b:21!null - │ │ ├── with-scan &1 - │ │ │ ├── columns: k:18!null r:19!null a:20!null b:21!null - │ │ │ └── mapping: - │ │ │ ├── column1:7 => k:18 - │ │ │ ├── column2:8 => r:19 - │ │ │ ├── column3:9 => a:20 - │ │ │ └── b_comp:10 => b:21 - │ │ ├── scan t - │ │ │ ├── columns: t.k:12!null t.r:13!null t.a:14 t.b:15 - │ │ │ ├── check constraint expressions - │ │ │ │ └── t.r:13 IN ('east', 'west') - │ │ │ ├── computed column expressions - │ │ │ │ └── t.b:15 - │ │ │ │ └── t.k:12 % 9 - │ │ │ └── flags: disabled not visible index feature - │ │ └── filters - │ │ ├── a:20 = t.a:14 - │ │ └── k:18 != t.k:12 - │ └── unique-checks-item: t(b) - │ └── project - │ ├── columns: b:37!null - │ └── semi-join (hash) - │ ├── columns: k:34!null r:35!null a:36!null b:37!null - │ ├── with-scan &1 - │ │ ├── columns: k:34!null r:35!null a:36!null b:37!null - │ │ └── mapping: - │ │ ├── column1:7 => k:34 - │ │ ├── column2:8 => r:35 - │ │ ├── column3:9 => a:36 - │ │ └── b_comp:10 => b:37 - │ ├── scan t - │ │ ├── columns: t.k:28!null t.r:29!null t.a:30 t.b:31 - │ │ ├── check constraint expressions - │ │ │ └── t.r:29 IN ('east', 'west') - │ │ ├── computed column expressions - │ │ │ └── t.b:31 - │ │ │ └── t.k:28 % 9 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ ├── b:37 = t.b:31 - │ └── k:34 != t.k:28 - └── fast-path-unique-checks - ├── fast-path-unique-checks-item: t(a) - │ └── select - │ ├── columns: t.k:22!null t.r:23!null t.a:24!null t.b:25 - │ ├── scan t - │ │ ├── columns: t.k:22!null t.r:23!null t.a:24 t.b:25 - │ │ ├── check constraint expressions - │ │ │ └── t.r:23 IN ('east', 'west') - │ │ ├── computed column expressions - │ │ │ └── t.b:25 - │ │ │ └── t.k:22 % 9 - │ │ └── flags: disabled not visible index feature - │ └── filters - │ └── t.a:24 = 10 - └── fast-path-unique-checks-item: t(b) - └── values + └── unique-checks + ├── unique-checks-item: t(a) + │ └── project + │ ├── columns: a:20!null + │ └── semi-join (hash) + │ ├── columns: k:18!null r:19!null a:20!null b:21!null + │ ├── with-scan &1 + │ │ ├── columns: k:18!null r:19!null a:20!null b:21!null + │ │ └── mapping: + │ │ ├── column1:7 => k:18 + │ │ ├── column2:8 => r:19 + │ │ ├── column3:9 => a:20 + │ │ └── b_comp:10 => b:21 + │ ├── scan t + │ │ ├── columns: t.k:12!null t.r:13!null t.a:14 t.b:15 + │ │ ├── check constraint expressions + │ │ │ └── t.r:13 IN ('east', 'west') + │ │ ├── computed column expressions + │ │ │ └── t.b:15 + │ │ │ └── t.k:12 % 9 + │ │ └── flags: disabled not visible index feature + │ └── filters + │ ├── a:20 = t.a:14 + │ └── k:18 != t.k:12 + └── unique-checks-item: t(b) + └── project + ├── columns: b:37!null + └── semi-join (hash) + ├── columns: k:34!null r:35!null a:36!null b:37!null + ├── with-scan &1 + │ ├── columns: k:34!null r:35!null a:36!null b:37!null + │ └── mapping: + │ ├── column1:7 => k:34 + │ ├── column2:8 => r:35 + │ ├── column3:9 => a:36 + │ └── b_comp:10 => b:37 + ├── scan t + │ ├── columns: t.k:28!null t.r:29!null t.a:30 t.b:31 + │ ├── check constraint expressions + │ │ └── t.r:29 IN ('east', 'west') + │ ├── computed column expressions + │ │ └── t.b:31 + │ │ └── t.k:28 % 9 + │ └── flags: disabled not visible index feature + └── filters + ├── b:37 = t.b:31 + └── k:34 != t.k:28 exec-ddl DROP TABLE t diff --git a/pkg/sql/opt/xform/coster.go b/pkg/sql/opt/xform/coster.go index 83d618920ed9..1380b67a738c 100644 --- a/pkg/sql/opt/xform/coster.go +++ b/pkg/sql/opt/xform/coster.go @@ -595,7 +595,7 @@ func (c *coster) ComputeCost(candidate memo.RelExpr, required *physical.Required case opt.InsertOp: insertExpr, _ := candidate.(*memo.InsertExpr) - if len(insertExpr.UniqueChecks) != 0 { + if len(insertExpr.FastPathUniqueChecks) != 0 { if len(insertExpr.FastPathUniqueChecks[0].DatumsFromConstraint) != 0 { // Make the cost of insert fast path slightly cheaper than non-fast path // so that the optimizer will pick it. All of the costed operations diff --git a/pkg/sql/opt/xform/testdata/rules/insert b/pkg/sql/opt/xform/testdata/rules/insert index b677009dd3d7..195ac2a6d4fc 100644 --- a/pkg/sql/opt/xform/testdata/rules/insert +++ b/pkg/sql/opt/xform/testdata/rules/insert @@ -36,18 +36,13 @@ insert t │ ├── key: () │ ├── fd: ()-->(7-12) │ └── (1, 'east', 10, 100, true, false) - ├── unique-checks - │ └── unique-checks-item: t(a) - │ └── values - │ ├── columns: a:21!null - │ ├── cardinality: [0 - 0] - │ ├── key: () - │ └── fd: ()-->(21) - └── fast-path-unique-checks - └── fast-path-unique-checks-item: t(a) + └── unique-checks + └── unique-checks-item: t(a) └── values + ├── columns: a:21!null ├── cardinality: [0 - 0] - └── key: () + ├── key: () + └── fd: ()-->(21) exec-ddl DROP TABLE t From c5d847f65f20b4193eee397369642ea7c8c5a0dc Mon Sep 17 00:00:00 2001 From: Yahor Yuzefovich Date: Thu, 28 Sep 2023 14:17:03 -0700 Subject: [PATCH 3/7] kvcoord: provide local node ID to DistSender in shared-process mode This commit makes it so that the DistSender of SQL pods running in shared-process mode have access to the node ID of the local KV node. The node ID is used to preferentially route requests to replicas available on the local node. Previously, this optimization was only available in single-tenant mode, and now it'll be also available in shared-process multi-tenant. The optimization doesn't exist in separate-process multi-tenant, so in that mode we continue to return `0` as the node ID which makes it so that replicas are picked randomly. For testing, this commit extends existing `TestSecondaryTenantFollowerReadsRouting` to also have a shared-process test configuration. There were a few gotchas in that adjustment: - for simplicity, we need to create a single region cluster so that latency and locality don't come into consideration when choosing the replica (we want to make sure that the local replica is used if available) - starting shared-process virtual cluster on a multi node cluster can be racy if we want to provide testing knobs, so we have to start the service on the gateway node first. (If we don't start with the gateway, then the server controller on the gateway could start the tenant server with empty knobs _before_ we explicitly start the service there.) - previously the test relied on retrieving the follower reads metric counters from all nodes and asserting only for `n2` it increased. However, in shared-process config it is now possible for an internal query (e.g. issued by auto stats collection) to be served via follower reads too, which would increment the counter confusing the test. As a result, this commit adjusts the test to audit the trace of the query and verify that the single follower read was served specifically by `n2`. Release note: None --- pkg/ccl/kvccl/kvfollowerreadsccl/BUILD.bazel | 1 - .../kvfollowerreadsccl/followerreads_test.go | 379 ++++++++++-------- pkg/kv/kvclient/kvcoord/dist_sender.go | 45 ++- .../kvclient/kvcoord/dist_sender_rangefeed.go | 2 +- pkg/kv/kvclient/kvcoord/dist_sender_test.go | 2 +- pkg/server/tenant.go | 15 +- 6 files changed, 255 insertions(+), 189 deletions(-) diff --git a/pkg/ccl/kvccl/kvfollowerreadsccl/BUILD.bazel b/pkg/ccl/kvccl/kvfollowerreadsccl/BUILD.bazel index d655ac1ff45a..5b39b39ca13c 100644 --- a/pkg/ccl/kvccl/kvfollowerreadsccl/BUILD.bazel +++ b/pkg/ccl/kvccl/kvfollowerreadsccl/BUILD.bazel @@ -48,7 +48,6 @@ go_test( deps = [ "//pkg/base", "//pkg/ccl", - "//pkg/ccl/kvccl/kvtenantccl", "//pkg/ccl/utilccl", "//pkg/keys", "//pkg/kv", diff --git a/pkg/ccl/kvccl/kvfollowerreadsccl/followerreads_test.go b/pkg/ccl/kvccl/kvfollowerreadsccl/followerreads_test.go index acab968d43cb..0623ddece5c2 100644 --- a/pkg/ccl/kvccl/kvfollowerreadsccl/followerreads_test.go +++ b/pkg/ccl/kvccl/kvfollowerreadsccl/followerreads_test.go @@ -19,11 +19,10 @@ import ( "time" "github.com/cockroachdb/cockroach/pkg/base" - // Blank import kvtenantccl so that we can create a tenant. - _ "github.com/cockroachdb/cockroach/pkg/ccl/kvccl/kvtenantccl" "github.com/cockroachdb/cockroach/pkg/ccl/utilccl" "github.com/cockroachdb/cockroach/pkg/keys" "github.com/cockroachdb/cockroach/pkg/kv" + "github.com/cockroachdb/cockroach/pkg/kv/kvbase" "github.com/cockroachdb/cockroach/pkg/kv/kvclient/kvcoord" "github.com/cockroachdb/cockroach/pkg/kv/kvpb" "github.com/cockroachdb/cockroach/pkg/kv/kvserver" @@ -872,195 +871,245 @@ func TestFollowerReadsWithStaleDescriptor(t *testing.T) { } // TestSecondaryTenantFollowerReadsRouting ensures that secondary tenants route -// their requests to the nearest replica. The test runs two versions -- one -// where accurate latency information between nodes is available and another -// where it needs to be estimated using node localities. +// their requests to the nearest replica. The test exercises three +// configurations: +// - shared-process multi-tenancy +// - separate-process multi-tenancy, with accurate latency information between +// nodes available +// - separate-process multi-tenancy, with accurate latency information between +// nodes unavailable which requires the fallback to estimates using node +// localities. +// +// For the shared-process multi-tenancy we set up a single region cluster so +// that locality information didn't come into play when choosing the replica. We +// use n2 as the gateway for the query and expect that its replica will serve +// the follower read. +// +// For the separate-process multi-tenancy we set up a three region cluster where +// n2 and n4 are in the same region, and we use n4 as the gateway and expect +// that n2 serves the follower read. func TestSecondaryTenantFollowerReadsRouting(t *testing.T) { defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) defer utilccl.TestingEnableEnterprise()() skip.UnderStressRace(t, "times out") - testutils.RunTrueAndFalse(t, "valid-latency-func", func(t *testing.T, validLatencyFunc bool) { - const numNodes = 4 - - serverArgs := make(map[int]base.TestServerArgs) - localities := make(map[int]roachpb.Locality) - for i := 0; i < numNodes; i++ { - regionName := fmt.Sprintf("region_%d", i) - if i == 3 { - // Make it such that n4 and n2 are in the same region. Below, we'll - // expect a follower read from n4 to be served by n2 because they're - // in the same locality (when validLatencyFunc is false). - regionName = fmt.Sprintf("region_%d", 1) - } - locality := roachpb.Locality{ - Tiers: []roachpb.Tier{{Key: "region", Value: regionName}}, - } - localities[i] = locality - serverArgs[i] = base.TestServerArgs{ - Locality: localities[i], + for _, testCase := range []struct { + name string + sharedProcess bool + validLatencyFunc bool + }{ + {name: "shared-process", sharedProcess: true}, + {name: "latency-based", sharedProcess: false, validLatencyFunc: true}, + {name: "locality-based", sharedProcess: false, validLatencyFunc: false}, + } { + t.Run(testCase.name, func(t *testing.T) { + const numNodes = 4 + gatewayNode := 3 + if testCase.sharedProcess { + gatewayNode = 1 } - } - tc := testcluster.StartTestCluster(t, numNodes, base.TestClusterArgs{ - ReplicationMode: base.ReplicationManual, - ServerArgsPerNode: serverArgs, - ServerArgs: base.TestServerArgs{ - DefaultTestTenant: base.TODOTestTenantDisabled, // we'll create one ourselves below. - }, - }) - ctx := context.Background() - defer tc.Stopper().Stop(ctx) - historicalQuery := `SELECT * FROM t.test AS OF SYSTEM TIME follower_read_timestamp() WHERE k=2` - recCh := make(chan tracingpb.Recording, 1) - - var tenants [numNodes]serverutils.ApplicationLayerInterface - for i := 0; i < numNodes; i++ { - knobs := base.TestingKnobs{} - if i == 3 { // n4 - knobs = base.TestingKnobs{ - KVClient: &kvcoord.ClientTestingKnobs{ - DontConsiderConnHealth: true, - // For the validLatencyFunc=true version of the test, the client - // pretends to have a low latency connection to n2. As a result, we - // expect n2 to be used for follower reads originating from n4. - // - // For the variant where no latency information is available, we - // expect n2 to serve follower reads as well, but because it - // is in the same locality as the client. - LatencyFunc: func(id roachpb.NodeID) (time.Duration, bool) { - if !validLatencyFunc { - return 0, false - } - if id == 2 { - return time.Millisecond, true - } - return 100 * time.Millisecond, true - }, - }, - SQLExecutor: &sql.ExecutorTestingKnobs{ - WithStatementTrace: func(trace tracingpb.Recording, stmt string) { - if stmt == historicalQuery { - recCh <- trace - } - }, - }, + serverArgs := make(map[int]base.TestServerArgs) + localities := make(map[int]roachpb.Locality) + for i := 0; i < numNodes; i++ { + regionName := fmt.Sprintf("region_%d", i) + if i == gatewayNode { + // Make it such that n4 and n2 are in the same region. + // Below, we'll expect a follower read from n4 to be served + // by n2 because they're in the same locality (when + // validLatencyFunc is false). + regionName = fmt.Sprintf("region_%d", 1) + } + if testCase.sharedProcess { + // In shared-process config we want all nodes to be in the + // same region so that other considerations (like latency + // function and locality matching don't come into play). + regionName = "test_region" + } + locality := roachpb.Locality{ + Tiers: []roachpb.Tier{{Key: "region", Value: regionName}}, + } + localities[i] = locality + serverArgs[i] = base.TestServerArgs{ + Locality: localities[i], } } - tt, err := tc.Server(i).TenantController().StartTenant(ctx, base.TestTenantArgs{ - TenantID: serverutils.TestTenantID(), - Locality: localities[i], - TestingKnobs: knobs, + tc := testcluster.StartTestCluster(t, numNodes, base.TestClusterArgs{ + ReplicationMode: base.ReplicationManual, + ServerArgsPerNode: serverArgs, + ServerArgs: base.TestServerArgs{ + DefaultTestTenant: base.TestControlsTenantsExplicitly, + }, }) - require.NoError(t, err) - tenants[i] = tt - } + ctx := context.Background() + defer tc.Stopper().Stop(ctx) - // Speed up closing of timestamps in order to sleep less below before we can - // use follower_read_timestamp(). Note that we need to override the setting - // for the tenant as well, because the builtin is run in the tenant's sql pod. - systemSQL := sqlutils.MakeSQLRunner(tc.Conns[0]) - systemSQL.Exec(t, `SET CLUSTER SETTING kv.closed_timestamp.target_duration = '0.1s'`) - systemSQL.Exec(t, `SET CLUSTER SETTING kv.closed_timestamp.side_transport_interval = '0.1s'`) - systemSQL.Exec(t, `SET CLUSTER SETTING kv.closed_timestamp.propagation_slack = '0.1s'`) - // We're making assertions on traces collected by the tenant using log lines - // in KV so we must ensure they're not redacted. - systemSQL.Exec(t, `SET CLUSTER SETTING trace.redact_at_virtual_cluster_boundary.enabled = 'false'`) + // Speed up closing of timestamps in order to sleep less below + // before we can use follower_read_timestamp(). Note that we need to + // override the setting for the tenant as well, because the builtin + // is run in the tenant's sql pod. + systemSQL := sqlutils.MakeSQLRunner(tc.Conns[0]) + systemSQL.Exec(t, `SET CLUSTER SETTING kv.closed_timestamp.target_duration = '0.1s'`) + systemSQL.Exec(t, `SET CLUSTER SETTING kv.closed_timestamp.side_transport_interval = '0.1s'`) + systemSQL.Exec(t, `SET CLUSTER SETTING kv.closed_timestamp.propagation_slack = '0.1s'`) + // We're making assertions on traces collected by the tenant using + // log lines in KV so we must ensure they're not redacted. + systemSQL.Exec(t, `SET CLUSTER SETTING trace.redact_at_virtual_cluster_boundary.enabled = 'false'`) - dbs := make([]*gosql.DB, numNodes) - for i := 0; i < numNodes; i++ { - dbs[i] = tenants[i].SQLConn(t, "") - } + historicalQuery := `SELECT * FROM t.test AS OF SYSTEM TIME follower_read_timestamp() WHERE k=2` + recCh := make(chan tracingpb.Recording, 1) - // Wait until all tenant servers are aware of the setting override. - testutils.SucceedsSoon(t, func() error { - settingNames := []string{ - "kv.closed_timestamp.target_duration", "kv.closed_timestamp.side_transport_interval", "kv.closed_timestamp.propagation_slack", + var tenants [numNodes]serverutils.ApplicationLayerInterface + dbs := make([]*gosql.DB, numNodes) + // In shared-process multi-tenancy we must initialize the tenant + // server on the gateway first in order to guarantee that the knobs + // are used (otherwise we have a race between the tenant server + // being started by the server controller and us explicitly starting + // the shared-process tenant server). + initOrder := []int{gatewayNode} + for i := 0; i < numNodes; i++ { + if i != gatewayNode { + initOrder = append(initOrder, i) + } } - for _, settingName := range settingNames { - for i := 0; i < numNodes; i++ { - db := dbs[i] - - var val string - err := db.QueryRow( - fmt.Sprintf("SHOW CLUSTER SETTING %s", settingName), - ).Scan(&val) - require.NoError(t, err) - if val != "00:00:00.1" { - return errors.Errorf("tenant server %d is still waiting for %s update: currently %s", - i, - settingName, - val, - ) + for _, i := range initOrder { + knobs := base.TestingKnobs{} + if i == gatewayNode { + knobs = base.TestingKnobs{ + KVClient: &kvcoord.ClientTestingKnobs{ + DontConsiderConnHealth: true, + // For the validLatencyFunc=true version of the + // test, the client pretends to have a low latency + // connection to n2. As a result, we expect n2 to be + // used for follower reads originating from n4. + // + // For the variant where no latency information is + // available, we expect n2 to serve follower reads + // as well, but because it is in the same locality + // as the client. + // + // Note that this latency func doesn't matter in the + // shared-process config. + LatencyFunc: func(id roachpb.NodeID) (time.Duration, bool) { + if !testCase.validLatencyFunc { + return 0, false + } + if id == 2 { + return time.Millisecond, true + } + return 100 * time.Millisecond, true + }, + }, + SQLExecutor: &sql.ExecutorTestingKnobs{ + WithStatementTrace: func(trace tracingpb.Recording, stmt string) { + if stmt == historicalQuery { + recCh <- trace + } + }, + }, } } + var err error + if testCase.sharedProcess { + tenants[i], dbs[i], err = tc.Server(i).TenantController().StartSharedProcessTenant(ctx, base.TestSharedProcessTenantArgs{ + TenantName: "test", + TenantID: serverutils.TestTenantID(), + Knobs: knobs, + }) + } else { + tenants[i], err = tc.Server(i).TenantController().StartTenant(ctx, base.TestTenantArgs{ + TenantID: serverutils.TestTenantID(), + Locality: localities[i], + TestingKnobs: knobs, + }) + dbs[i] = tenants[i].SQLConn(t, "") + } + require.NoError(t, err) } - return nil - }) - tenantSQLDB := dbs[3] - tenantSQL := sqlutils.MakeSQLRunner(tenantSQLDB) + // Wait until all tenant servers are aware of the setting override. + testutils.SucceedsSoon(t, func() error { + settingNames := []string{ + "kv.closed_timestamp.target_duration", "kv.closed_timestamp.side_transport_interval", "kv.closed_timestamp.propagation_slack", + } + for _, settingName := range settingNames { + for i := 0; i < numNodes; i++ { + db := dbs[i] - tenantSQL.Exec(t, `CREATE DATABASE t`) - tenantSQL.Exec(t, `CREATE TABLE t.test (k INT PRIMARY KEY)`) + var val string + err := db.QueryRow( + fmt.Sprintf("SHOW CLUSTER SETTING %s", settingName), + ).Scan(&val) + require.NoError(t, err) + if val != "00:00:00.1" { + return errors.Errorf("tenant server %d is still waiting for %s update: currently %s", + i, + settingName, + val, + ) + } + } + } + return nil + }) - startKey := keys.MakeSQLCodec(serverutils.TestTenantID()).TenantPrefix() - tc.AddVotersOrFatal(t, startKey, tc.Target(1), tc.Target(2)) - tc.WaitForVotersOrFatal(t, startKey, tc.Target(1), tc.Target(2)) - desc := tc.LookupRangeOrFatal(t, startKey) - require.Equal(t, []roachpb.ReplicaDescriptor{ - {NodeID: 1, StoreID: 1, ReplicaID: 1}, - {NodeID: 2, StoreID: 2, ReplicaID: 2}, - {NodeID: 3, StoreID: 3, ReplicaID: 3}, - }, desc.Replicas().Descriptors()) + tenantSQLDB := dbs[gatewayNode] + tenantSQL := sqlutils.MakeSQLRunner(tenantSQLDB) - // Sleep so that we can perform follower reads. The read timestamp needs to be - // above the timestamp when the table was created. - log.Infof(ctx, "test sleeping for the follower read timestamps to pass the table creation timestamp...") - time.Sleep(500 * time.Millisecond) - log.Infof(ctx, "test sleeping... done") + tenantSQL.Exec(t, `CREATE DATABASE t`) + tenantSQL.Exec(t, `CREATE TABLE t.test (k INT PRIMARY KEY)`) - getFollowerReadCounts := func() [numNodes]int64 { - var counts [numNodes]int64 - for i := range tc.Servers { - err := tc.Servers[i].GetStores().(*kvserver.Stores).VisitStores(func(s *kvserver.Store) error { - counts[i] = s.Metrics().FollowerReadsCount.Count() - return nil - }) - require.NoError(t, err) - } - return counts - } + codec := tenants[gatewayNode].Codec() + startKey := codec.TenantPrefix() + tc.AddVotersOrFatal(t, startKey, tc.Target(1), tc.Target(2)) + tc.WaitForVotersOrFatal(t, startKey, tc.Target(1), tc.Target(2)) + desc := tc.LookupRangeOrFatal(t, startKey) + require.Equal(t, []roachpb.ReplicaDescriptor{ + {NodeID: 1, StoreID: 1, ReplicaID: 1}, + {NodeID: 2, StoreID: 2, ReplicaID: 2}, + {NodeID: 3, StoreID: 3, ReplicaID: 3}, + }, desc.Replicas().Descriptors()) - // Check that the cache was indeed populated. - tenantSQL.Exec(t, `SELECT * FROM t.test WHERE k = 1`) - tablePrefix := keys.MustAddr(keys.MakeSQLCodec(serverutils.TestTenantID()).TenantPrefix()) - cache := tenants[3].DistSenderI().(*kvcoord.DistSender).RangeDescriptorCache() - entry := cache.GetCached(ctx, tablePrefix, false /* inverted */) - require.NotNil(t, entry) - require.False(t, entry.Lease().Empty()) - require.Equal(t, roachpb.StoreID(1), entry.Lease().Replica.StoreID) - require.Equal(t, []roachpb.ReplicaDescriptor{ - {NodeID: 1, StoreID: 1, ReplicaID: 1}, - {NodeID: 2, StoreID: 2, ReplicaID: 2}, - {NodeID: 3, StoreID: 3, ReplicaID: 3}, - }, entry.Desc().Replicas().Descriptors()) + // Sleep so that we can perform follower reads. The read timestamp + // needs to be above the timestamp when the table was created. + log.Infof(ctx, "test sleeping for the follower read timestamps to pass the table creation timestamp...") + time.Sleep(500 * time.Millisecond) + log.Infof(ctx, "test sleeping... done") - followerReadCountsBefore := getFollowerReadCounts() - tenantSQL.Exec(t, historicalQuery) - followerReadsCountsAfter := getFollowerReadCounts() + // Check that the cache was indeed populated. + tenantSQL.Exec(t, `SELECT * FROM t.test WHERE k = 1`) + tablePrefix := keys.MustAddr(codec.TenantPrefix()) + cache := tenants[gatewayNode].DistSenderI().(*kvcoord.DistSender).RangeDescriptorCache() + entry := cache.GetCached(ctx, tablePrefix, false /* inverted */) + require.NotNil(t, entry) + require.False(t, entry.Lease().Empty()) + require.Equal(t, roachpb.StoreID(1), entry.Lease().Replica.StoreID) + require.Equal(t, []roachpb.ReplicaDescriptor{ + {NodeID: 1, StoreID: 1, ReplicaID: 1}, + {NodeID: 2, StoreID: 2, ReplicaID: 2}, + {NodeID: 3, StoreID: 3, ReplicaID: 3}, + }, entry.Desc().Replicas().Descriptors()) - rec := <-recCh - // Look at the trace and check that we've served a follower read. - require.True(t, kv.OnlyFollowerReads(rec), "query was served through follower reads: %s", rec) + tenantSQL.Exec(t, historicalQuery) + rec := <-recCh - for i := 0; i < numNodes; i++ { - if i == 1 { // n2 - require.Greater(t, followerReadsCountsAfter[i], followerReadCountsBefore[i]) - continue + // Look at the trace and check that the follower read was served by + // n2. + var numFRs, numN2FRs int + for _, sp := range rec { + for _, l := range sp.Logs { + if msg := l.Message.StripMarkers(); strings.Contains(msg, kvbase.FollowerReadServingMsg) { + numFRs++ + if strings.Contains(msg, "n2") { + numN2FRs++ + } + } + } } - require.Equal(t, followerReadsCountsAfter[i], followerReadCountsBefore[i]) - } - }) + require.Equal(t, numFRs, 1, "query wasn't served through follower reads: %s", rec) + require.Equal(t, numN2FRs, 1, "follower read wasn't served by n2: %s", rec) + }) + } } diff --git a/pkg/kv/kvclient/kvcoord/dist_sender.go b/pkg/kv/kvclient/kvcoord/dist_sender.go index cdc60c607e24..392b917482f6 100644 --- a/pkg/kv/kvclient/kvcoord/dist_sender.go +++ b/pkg/kv/kvclient/kvcoord/dist_sender.go @@ -502,6 +502,11 @@ type DistSender struct { // nodeDescs provides information on the KV nodes that DistSender may // consider routing requests to. nodeDescs NodeDescStore + // nodeIDGetter provides access to the local KV node ID if it's available + // (0 otherwise). The default implementation uses the Gossip network if it's + // available, but custom implementation can be provided via + // DistSenderConfig.NodeIDGetter. + nodeIDGetter func() roachpb.NodeID // metrics stored DistSender-related metrics. metrics DistSenderMetrics // rangeCache caches replica metadata for key ranges. @@ -568,6 +573,10 @@ type DistSenderConfig struct { Settings *cluster.Settings Clock *hlc.Clock NodeDescs NodeDescStore + // NodeIDGetter, if set, provides non-gossip based implementation for + // obtaining the local KV node ID. The DistSender uses the node ID to + // preferentially route requests to a local replica (if one exists). + NodeIDGetter func() roachpb.NodeID // nodeDescriptor, if provided, is used to describe which node the // DistSender lives on, for instance when deciding where to send RPCs. // Usually it is filled in from the Gossip network on demand. @@ -611,10 +620,22 @@ type DistSenderConfig struct { // DistSenderContext or the fields within is optional. For omitted values, sane // defaults will be used. func NewDistSender(cfg DistSenderConfig) *DistSender { + nodeIDGetter := cfg.NodeIDGetter + if nodeIDGetter == nil { + // Fallback to gossip-based implementation if other is not provided. + nodeIDGetter = func() roachpb.NodeID { + g, ok := cfg.NodeDescs.(*gossip.Gossip) + if !ok { + return 0 + } + return g.NodeID.Get() + } + } ds := &DistSender{ st: cfg.Settings, clock: cfg.Clock, nodeDescs: cfg.NodeDescs, + nodeIDGetter: nodeIDGetter, metrics: makeDistSenderMetrics(), kvInterceptor: cfg.KVInterceptor, locality: cfg.Locality, @@ -778,22 +799,6 @@ func (ds *DistSender) FirstRange() (*roachpb.RangeDescriptor, error) { return ds.firstRangeProvider.GetFirstRangeDescriptor() } -// getNodeID attempts to return the local node ID. It returns 0 if the DistSender -// does not have access to the Gossip network. -func (ds *DistSender) getNodeID() roachpb.NodeID { - // Today, secondary tenants don't run in process with KV instances, so they - // don't have access to the Gossip network. The DistSender uses the node ID to - // preferentially route requests to a local replica (if one exists). Not - // knowing the node ID, and thus not being able to take advantage of this - // optimization is okay, given tenants not running in-process with KV - // instances have no such optimization to take advantage of to begin with. - g, ok := ds.nodeDescs.(*gossip.Gossip) - if !ok { - return 0 - } - return g.NodeID.Get() -} - // CountRanges returns the number of ranges that encompass the given key span. func (ds *DistSender) CountRanges(ctx context.Context, rs roachpb.RSpan) (int64, error) { var count int64 @@ -859,7 +864,7 @@ func (ds *DistSender) getRoutingInfo( func (ds *DistSender) initAndVerifyBatch(ctx context.Context, ba *kvpb.BatchRequest) *kvpb.Error { // Attach the local node ID to each request. if ba.GatewayNodeID == 0 { - ba.GatewayNodeID = ds.getNodeID() + ba.GatewayNodeID = ds.nodeIDGetter() } // Attach a clock reading from the local node to help stabilize HLCs across @@ -2235,7 +2240,7 @@ func (ds *DistSender) sendToReplicas( // First order by latency, then move the leaseholder to the front of the // list, if it is known. if !ds.dontReorderReplicas { - replicas.OptimizeReplicaOrder(ds.getNodeID(), ds.latencyFunc, ds.locality) + replicas.OptimizeReplicaOrder(ds.nodeIDGetter(), ds.latencyFunc, ds.locality) } idx := -1 @@ -2254,7 +2259,7 @@ func (ds *DistSender) sendToReplicas( case kvpb.RoutingPolicy_NEAREST: // Order by latency. log.VEvent(ctx, 2, "routing to nearest replica; leaseholder not required") - replicas.OptimizeReplicaOrder(ds.getNodeID(), ds.latencyFunc, ds.locality) + replicas.OptimizeReplicaOrder(ds.nodeIDGetter(), ds.latencyFunc, ds.locality) default: log.Fatalf(ctx, "unknown routing policy: %s", ba.RoutingPolicy) @@ -2374,7 +2379,7 @@ func (ds *DistSender) sendToReplicas( (desc.Generation == 0 && routing.LeaseSeq() == 0), } - comparisonResult := ds.getLocalityComparison(ctx, ds.getNodeID(), ba.Replica.NodeID) + comparisonResult := ds.getLocalityComparison(ctx, ds.nodeIDGetter(), ba.Replica.NodeID) ds.metrics.updateCrossLocalityMetricsOnReplicaAddressedBatchRequest(comparisonResult, int64(ba.Size())) br, err = transport.SendNext(ctx, ba) diff --git a/pkg/kv/kvclient/kvcoord/dist_sender_rangefeed.go b/pkg/kv/kvclient/kvcoord/dist_sender_rangefeed.go index 4d10f4af9d2e..97ee1c85e7a9 100644 --- a/pkg/kv/kvclient/kvcoord/dist_sender_rangefeed.go +++ b/pkg/kv/kvclient/kvcoord/dist_sender_rangefeed.go @@ -720,7 +720,7 @@ func newTransportForRange( if err != nil { return nil, err } - replicas.OptimizeReplicaOrder(ds.getNodeID(), latencyFn, ds.locality) + replicas.OptimizeReplicaOrder(ds.nodeIDGetter(), latencyFn, ds.locality) opts := SendOptions{class: connectionClass(&ds.st.SV)} return ds.transportFactory(opts, ds.nodeDialer, replicas) } diff --git a/pkg/kv/kvclient/kvcoord/dist_sender_test.go b/pkg/kv/kvclient/kvcoord/dist_sender_test.go index 27ee10f1a8dd..5f8ad1fb258d 100644 --- a/pkg/kv/kvclient/kvcoord/dist_sender_test.go +++ b/pkg/kv/kvclient/kvcoord/dist_sender_test.go @@ -617,7 +617,7 @@ func TestImmutableBatchArgs(t *testing.T) { txn := roachpb.MakeTransaction( "test", nil /* baseKey */, isolation.Serializable, roachpb.NormalUserPriority, - clock.Now(), clock.MaxOffset().Nanoseconds(), int32(ds.getNodeID()), 0, + clock.Now(), clock.MaxOffset().Nanoseconds(), int32(ds.nodeIDGetter()), 0, ) origTxnTs := txn.WriteTimestamp diff --git a/pkg/server/tenant.go b/pkg/server/tenant.go index bc71abb5990e..28dbd6c64fb6 100644 --- a/pkg/server/tenant.go +++ b/pkg/server/tenant.go @@ -175,6 +175,7 @@ func (s *SQLServerWrapper) Drain( // process tenant. type tenantServerDeps struct { instanceIDContainer *base.SQLIDContainer + nodeIDGetter func() roachpb.NodeID // The following should eventually be connected to tenant // capabilities. @@ -199,7 +200,14 @@ func NewSeparateProcessTenantServer( tenantNameContainer *roachpb.TenantNameContainer, ) (*SQLServerWrapper, error) { deps := tenantServerDeps{ - instanceIDContainer: baseCfg.IDContainer.SwitchToSQLIDContainerForStandaloneSQLInstance(), + instanceIDContainer: baseCfg.IDContainer.SwitchToSQLIDContainerForStandaloneSQLInstance(), + // The kvcoord.DistSender uses the node ID to preferentially route + // requests to a local replica (if one exists). In separate-process + // mode, not knowing the node ID, and thus not being able to take + // advantage of this optimization is okay, given tenants not running + // in-process with KV instances have no such optimization to take + // advantage of to begin with. + nodeIDGetter: nil, costControllerFactory: NewTenantSideCostController, spanLimiterFactory: func(ie isql.Executor, st *cluster.Settings, knobs *spanconfig.TestingKnobs) spanconfig.Limiter { return spanconfiglimiter.New(ie, st, knobs) @@ -228,6 +236,10 @@ func newSharedProcessTenantServer( deps := tenantServerDeps{ instanceIDContainer: base.NewSQLIDContainerForNode(baseCfg.IDContainer), + // The kvcoord.DistSender uses the node ID to preferentially route + // requests to a local replica (if one exists). In shared-process mode + // we can easily provide that without accessing the gossip. + nodeIDGetter: baseCfg.IDContainer.Get, // TODO(ssd): The cost controller should instead be able to // read from the capability system and return immediately if // the tenant is exempt. For now we are turning off the @@ -1140,6 +1152,7 @@ func makeTenantSQLServerArgs( Settings: st, Clock: clock, NodeDescs: tenantConnect, + NodeIDGetter: deps.nodeIDGetter, RPCRetryOptions: &rpcRetryOptions, RPCContext: rpcContext, NodeDialer: kvNodeDialer, From a9af2c7c83534f5b843f0d02e00859df93fe0c12 Mon Sep 17 00:00:00 2001 From: rharding6373 Date: Mon, 18 Sep 2023 21:00:40 -0700 Subject: [PATCH 4/7] sql: anonymize function names for error reporting and telemetry Previously, we did not anonymize/redact function names because we only supported built-in, CRDB-named functions. However, since adding support for UDFs, function names may be user-provided. We need to omit user-provided names in error reporting and telemetry. We fix this oversight in this PR by no longer overriding the anonymizing format flag and redaction flag for FuncExprs. Unfortunately, since any functions in a query are not always resolved when the error occurs, we may also redact built-in function names. Epic: None Fixes: None Release note (sql change): Anonymizes function names in error reporting and telemetry so that user-provided names for UDFs are not included. --- pkg/sql/event_log_test.go | 5 +- pkg/sql/parser/testdata/call | 4 +- pkg/sql/parser/testdata/control_job | 4 +- pkg/sql/parser/testdata/create_index | 34 ++-- pkg/sql/parser/testdata/create_table | 2 +- pkg/sql/parser/testdata/import_export | 2 +- pkg/sql/parser/testdata/predefined_functions | 4 +- pkg/sql/parser/testdata/reserved_keywords | 4 +- pkg/sql/parser/testdata/select_clauses | 192 +++++++++--------- pkg/sql/parser/testdata/select_exprs | 8 +- .../parser/testdata/user_defined_functions | 40 ++++ .../scplan/testdata/alter_table_add_check_udf | 2 +- .../alter_table_add_check_udf.explain | 2 +- .../alter_table_add_check_udf.explain_shape | 2 +- .../alter_table_add_check_udf.side_effects | 6 +- ...ble_add_check_udf__rollback_1_of_2.explain | 2 +- ...ble_add_check_udf__rollback_2_of_2.explain | 2 +- pkg/sql/sem/tree/expr.go | 12 +- pkg/sql/sem/tree/format.go | 4 +- pkg/sql/sem/tree/function_definition.go | 4 + pkg/sql/sem/tree/name_part.go | 23 ++- pkg/sql/statement_mark_redaction_test.go | 10 +- 22 files changed, 211 insertions(+), 157 deletions(-) create mode 100644 pkg/sql/parser/testdata/user_defined_functions diff --git a/pkg/sql/event_log_test.go b/pkg/sql/event_log_test.go index 700731e85d91..395d00f9408e 100644 --- a/pkg/sql/event_log_test.go +++ b/pkg/sql/event_log_test.go @@ -159,7 +159,7 @@ func TestPerfLogging(t *testing.T) { { query: `INSERT INTO t VALUES (1, pg_sleep(0.256), 'x')`, errRe: `duplicate key`, - logRe: `"EventType":"slow_query","Statement":"INSERT INTO .*‹t› VALUES \(‹1›, pg_sleep\(‹0.256›\), ‹'x'›\)","Tag":"INSERT","User":"root"`, + logRe: `"EventType":"slow_query","Statement":"INSERT INTO .*‹t› VALUES \(‹1›, ‹pg_sleep›\(‹0.256›\), ‹'x'›\)","Tag":"INSERT","User":"root"`, logExpected: true, channel: channel.SQL_PERF, }, @@ -743,6 +743,9 @@ func TestPerfLogging(t *testing.T) { if err != nil { t.Fatal(err) } + for _, l := range entries { + log.Infof(context.Background(), "%s", l.Message) + } if (len(entries) > 0) != tc.logExpected { expected := "at least one message" diff --git a/pkg/sql/parser/testdata/call b/pkg/sql/parser/testdata/call index c3d7ff727705..afe2053b711a 100644 --- a/pkg/sql/parser/testdata/call +++ b/pkg/sql/parser/testdata/call @@ -4,7 +4,7 @@ CALL p() CALL p() CALL p() -- fully parenthesized CALL p() -- literals removed -CALL p() -- identifiers removed +CALL _() -- identifiers removed parse CALL p(1, 'foo', 1.234, true, NULL) @@ -12,7 +12,7 @@ CALL p(1, 'foo', 1.234, true, NULL) CALL p(1, 'foo', 1.234, true, NULL) CALL p((1), ('foo'), (1.234), (true), (NULL)) -- fully parenthesized CALL p(_, '_', _, _, _) -- literals removed -CALL p(1, 'foo', 1.234, true, NULL) -- identifiers removed +CALL _(1, 'foo', 1.234, true, NULL) -- identifiers removed error CALL p diff --git a/pkg/sql/parser/testdata/control_job b/pkg/sql/parser/testdata/control_job index fedf1aa6b545..48c892d6bc01 100644 --- a/pkg/sql/parser/testdata/control_job +++ b/pkg/sql/parser/testdata/control_job @@ -304,7 +304,7 @@ RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) RESUME JOBS FOR SCHEDULES SELECT (unnest((ARRAY[(1), (2), (3)]))) -- fully parenthesized RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[_, _, __more1_10__]) -- literals removed -RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) -- identifiers removed +RESUME JOBS FOR SCHEDULES SELECT _(ARRAY[1, 2, 3]) -- identifiers removed parse EXPLAIN RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) @@ -312,7 +312,7 @@ EXPLAIN RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) EXPLAIN RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) EXPLAIN RESUME JOBS FOR SCHEDULES SELECT (unnest((ARRAY[(1), (2), (3)]))) -- fully parenthesized EXPLAIN RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[_, _, __more1_10__]) -- literals removed -EXPLAIN RESUME JOBS FOR SCHEDULES SELECT unnest(ARRAY[1, 2, 3]) -- identifiers removed +EXPLAIN RESUME JOBS FOR SCHEDULES SELECT _(ARRAY[1, 2, 3]) -- identifiers removed parse RESUME JOB a diff --git a/pkg/sql/parser/testdata/create_index b/pkg/sql/parser/testdata/create_index index ed4190a542bd..04b466d1a54d 100644 --- a/pkg/sql/parser/testdata/create_index +++ b/pkg/sql/parser/testdata/create_index @@ -226,7 +226,7 @@ CREATE INDEX ON a (lower(a)) CREATE INDEX ON a (lower(a)) CREATE INDEX ON a ((lower((a)))) -- fully parenthesized CREATE INDEX ON a (lower(a)) -- literals removed -CREATE INDEX ON _ (lower(_)) -- identifiers removed +CREATE INDEX ON _ (_(_)) -- identifiers removed parse CREATE INDEX ON a (a, lower(b)) @@ -234,7 +234,7 @@ CREATE INDEX ON a (a, lower(b)) CREATE INDEX ON a (a, lower(b)) CREATE INDEX ON a (a, (lower((b)))) -- fully parenthesized CREATE INDEX ON a (a, lower(b)) -- literals removed -CREATE INDEX ON _ (_, lower(_)) -- identifiers removed +CREATE INDEX ON _ (_, _(_)) -- identifiers removed parse CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) @@ -242,7 +242,7 @@ CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE INDEX ON a (((((((lower((a))) || (' ')))) || (lower((b)))))) -- fully parenthesized CREATE INDEX ON a (((lower(a) || '_') || lower(b))) -- literals removed -CREATE INDEX ON _ (((lower(_) || ' ') || lower(_))) -- identifiers removed +CREATE INDEX ON _ (((_(_) || ' ') || _(_))) -- identifiers removed parse CREATE INDEX ON a (a, (a + 1), (b + 2)) @@ -258,7 +258,7 @@ CREATE INDEX ON a (lower(a)) CREATE INDEX ON a (lower(a)) CREATE INDEX ON a ((lower((a)))) -- fully parenthesized CREATE INDEX ON a (lower(a)) -- literals removed -CREATE INDEX ON _ (lower(_)) -- identifiers removed +CREATE INDEX ON _ (_(_)) -- identifiers removed parse CREATE INDEX ON a (lower(a), lower(b)) @@ -266,7 +266,7 @@ CREATE INDEX ON a (lower(a), lower(b)) CREATE INDEX ON a (lower(a), lower(b)) CREATE INDEX ON a ((lower((a))), (lower((b)))) -- fully parenthesized CREATE INDEX ON a (lower(a), lower(b)) -- literals removed -CREATE INDEX ON _ (lower(_), lower(_)) -- identifiers removed +CREATE INDEX ON _ (_(_), _(_)) -- identifiers removed parse CREATE INDEX ON a (a, lower(b)) @@ -274,7 +274,7 @@ CREATE INDEX ON a (a, lower(b)) CREATE INDEX ON a (a, lower(b)) CREATE INDEX ON a (a, (lower((b)))) -- fully parenthesized CREATE INDEX ON a (a, lower(b)) -- literals removed -CREATE INDEX ON _ (_, lower(_)) -- identifiers removed +CREATE INDEX ON _ (_, _(_)) -- identifiers removed parse CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) @@ -282,7 +282,7 @@ CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE INDEX ON a (((((((lower((a))) || (' ')))) || (lower((b)))))) -- fully parenthesized CREATE INDEX ON a (((lower(a) || '_') || lower(b))) -- literals removed -CREATE INDEX ON _ (((lower(_) || ' ') || lower(_))) -- identifiers removed +CREATE INDEX ON _ (((_(_) || ' ') || _(_))) -- identifiers removed parse CREATE INDEX ON a (a, (lower(b))) @@ -290,7 +290,7 @@ CREATE INDEX ON a (a, (lower(b))) CREATE INDEX ON a (a, lower(b)) -- normalized! CREATE INDEX ON a (a, (lower((b)))) -- fully parenthesized CREATE INDEX ON a (a, lower(b)) -- literals removed -CREATE INDEX ON _ (_, lower(_)) -- identifiers removed +CREATE INDEX ON _ (_, _(_)) -- identifiers removed parse CREATE INDEX ON a ((lower(a) || ' ' || lower(b))) @@ -298,7 +298,7 @@ CREATE INDEX ON a ((lower(a) || ' ' || lower(b))) CREATE INDEX ON a (((lower(a) || ' ') || lower(b))) -- normalized! CREATE INDEX ON a ((((((lower((a))) || (' '))) || (lower((b)))))) -- fully parenthesized CREATE INDEX ON a (((lower(a) || '_') || lower(b))) -- literals removed -CREATE INDEX ON _ (((lower(_) || ' ') || lower(_))) -- identifiers removed +CREATE INDEX ON _ (((_(_) || ' ') || _(_))) -- identifiers removed parse CREATE UNIQUE INDEX ON a ((a + b)) @@ -314,7 +314,7 @@ CREATE UNIQUE INDEX ON a (lower(a)) CREATE UNIQUE INDEX ON a (lower(a)) CREATE UNIQUE INDEX ON a ((lower((a)))) -- fully parenthesized CREATE UNIQUE INDEX ON a (lower(a)) -- literals removed -CREATE UNIQUE INDEX ON _ (lower(_)) -- identifiers removed +CREATE UNIQUE INDEX ON _ (_(_)) -- identifiers removed parse CREATE UNIQUE INDEX ON a (a, lower(b)) @@ -322,7 +322,7 @@ CREATE UNIQUE INDEX ON a (a, lower(b)) CREATE UNIQUE INDEX ON a (a, lower(b)) CREATE UNIQUE INDEX ON a (a, (lower((b)))) -- fully parenthesized CREATE UNIQUE INDEX ON a (a, lower(b)) -- literals removed -CREATE UNIQUE INDEX ON _ (_, lower(_)) -- identifiers removed +CREATE UNIQUE INDEX ON _ (_, _(_)) -- identifiers removed parse CREATE UNIQUE INDEX ON a (((lower(a) || ' ') || lower(b))) @@ -330,7 +330,7 @@ CREATE UNIQUE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE UNIQUE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE UNIQUE INDEX ON a (((((((lower((a))) || (' ')))) || (lower((b)))))) -- fully parenthesized CREATE UNIQUE INDEX ON a (((lower(a) || '_') || lower(b))) -- literals removed -CREATE UNIQUE INDEX ON _ (((lower(_) || ' ') || lower(_))) -- identifiers removed +CREATE UNIQUE INDEX ON _ (((_(_) || ' ') || _(_))) -- identifiers removed parse CREATE UNIQUE INDEX ON a (a, (a + 1), (b + 2)) @@ -346,7 +346,7 @@ CREATE UNIQUE INDEX ON a (lower(a)) CREATE UNIQUE INDEX ON a (lower(a)) CREATE UNIQUE INDEX ON a ((lower((a)))) -- fully parenthesized CREATE UNIQUE INDEX ON a (lower(a)) -- literals removed -CREATE UNIQUE INDEX ON _ (lower(_)) -- identifiers removed +CREATE UNIQUE INDEX ON _ (_(_)) -- identifiers removed parse CREATE UNIQUE INDEX ON a (lower(a), lower(b)) @@ -354,7 +354,7 @@ CREATE UNIQUE INDEX ON a (lower(a), lower(b)) CREATE UNIQUE INDEX ON a (lower(a), lower(b)) CREATE UNIQUE INDEX ON a ((lower((a))), (lower((b)))) -- fully parenthesized CREATE UNIQUE INDEX ON a (lower(a), lower(b)) -- literals removed -CREATE UNIQUE INDEX ON _ (lower(_), lower(_)) -- identifiers removed +CREATE UNIQUE INDEX ON _ (_(_), _(_)) -- identifiers removed parse CREATE UNIQUE INDEX ON a (a, lower(b)) @@ -362,7 +362,7 @@ CREATE UNIQUE INDEX ON a (a, lower(b)) CREATE UNIQUE INDEX ON a (a, lower(b)) CREATE UNIQUE INDEX ON a (a, (lower((b)))) -- fully parenthesized CREATE UNIQUE INDEX ON a (a, lower(b)) -- literals removed -CREATE UNIQUE INDEX ON _ (_, lower(_)) -- identifiers removed +CREATE UNIQUE INDEX ON _ (_, _(_)) -- identifiers removed parse CREATE UNIQUE INDEX ON a (((lower(a) || ' ') || lower(b))) @@ -370,7 +370,7 @@ CREATE UNIQUE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE UNIQUE INDEX ON a (((lower(a) || ' ') || lower(b))) CREATE UNIQUE INDEX ON a (((((((lower((a))) || (' ')))) || (lower((b)))))) -- fully parenthesized CREATE UNIQUE INDEX ON a (((lower(a) || '_') || lower(b))) -- literals removed -CREATE UNIQUE INDEX ON _ (((lower(_) || ' ') || lower(_))) -- identifiers removed +CREATE UNIQUE INDEX ON _ (((_(_) || ' ') || _(_))) -- identifiers removed parse CREATE INVERTED INDEX ON a ((ARRAY[a, b])) @@ -427,7 +427,7 @@ CREATE UNIQUE INDEX idx ON a (((lower(a) || ' ') || lower(b))) NOT VISIBLE CREATE UNIQUE INDEX idx ON a (((lower(a) || ' ') || lower(b))) NOT VISIBLE CREATE UNIQUE INDEX idx ON a (((((((lower((a))) || (' ')))) || (lower((b)))))) NOT VISIBLE -- fully parenthesized CREATE UNIQUE INDEX idx ON a (((lower(a) || '_') || lower(b))) NOT VISIBLE -- literals removed -CREATE UNIQUE INDEX _ ON _ (((lower(_) || ' ') || lower(_))) NOT VISIBLE -- identifiers removed +CREATE UNIQUE INDEX _ ON _ (((_(_) || ' ') || _(_))) NOT VISIBLE -- identifiers removed parse CREATE INVERTED INDEX IF NOT EXISTS a ON b (c) WHERE d > 3 VISIBLE diff --git a/pkg/sql/parser/testdata/create_table b/pkg/sql/parser/testdata/create_table index 91cee45a794a..c61982935662 100644 --- a/pkg/sql/parser/testdata/create_table +++ b/pkg/sql/parser/testdata/create_table @@ -631,7 +631,7 @@ CREATE TABLE a (b INT8 DEFAULT now()) CREATE TABLE a (b INT8 DEFAULT now()) CREATE TABLE a (b INT8 DEFAULT (now())) -- fully parenthesized CREATE TABLE a (b INT8 DEFAULT now()) -- literals removed -CREATE TABLE _ (_ INT8 DEFAULT now()) -- identifiers removed +CREATE TABLE _ (_ INT8 DEFAULT _()) -- identifiers removed error CREATE TABLE test ( diff --git a/pkg/sql/parser/testdata/import_export b/pkg/sql/parser/testdata/import_export index a1863dd52b63..22d0e5e65a13 100644 --- a/pkg/sql/parser/testdata/import_export +++ b/pkg/sql/parser/testdata/import_export @@ -92,4 +92,4 @@ EXPORT INTO CSV 's3://my/path/%part%.csv' WITH delimiter = '|' FROM SELECT a, su EXPORT INTO CSV 's3://my/path/%part%.csv' WITH OPTIONS(delimiter = '|') FROM SELECT a, sum(b) FROM c WHERE d = 1 ORDER BY sum(b) DESC LIMIT 10 -- normalized! EXPORT INTO CSV ('s3://my/path/%part%.csv') WITH OPTIONS(delimiter = ('|')) FROM SELECT (a), (sum((b))) FROM c WHERE ((d) = (1)) ORDER BY (sum((b))) DESC LIMIT (10) -- fully parenthesized EXPORT INTO CSV '_' WITH OPTIONS(delimiter = '_') FROM SELECT a, sum(b) FROM c WHERE d = _ ORDER BY sum(b) DESC LIMIT _ -- literals removed -EXPORT INTO CSV 's3://my/path/%part%.csv' WITH OPTIONS(_ = '|') FROM SELECT _, sum(_) FROM _ WHERE _ = 1 ORDER BY sum(_) DESC LIMIT 10 -- identifiers removed +EXPORT INTO CSV 's3://my/path/%part%.csv' WITH OPTIONS(_ = '|') FROM SELECT _, _(_) FROM _ WHERE _ = 1 ORDER BY _(_) DESC LIMIT 10 -- identifiers removed diff --git a/pkg/sql/parser/testdata/predefined_functions b/pkg/sql/parser/testdata/predefined_functions index 6713de8f9b42..b9ea4a1c3cdc 100644 --- a/pkg/sql/parser/testdata/predefined_functions +++ b/pkg/sql/parser/testdata/predefined_functions @@ -213,7 +213,7 @@ SELECT EXTRACT(second from now()) SELECT extract('second', now()) -- normalized! SELECT (extract(('second'), (now()))) -- fully parenthesized SELECT extract('_', now()) -- literals removed -SELECT extract('second', now()) -- identifiers removed +SELECT extract('second', _()) -- identifiers removed parse SELECT EXTRACT('second' from now()) @@ -221,7 +221,7 @@ SELECT EXTRACT('second' from now()) SELECT extract('second', now()) -- normalized! SELECT (extract(('second'), (now()))) -- fully parenthesized SELECT extract('_', now()) -- literals removed -SELECT extract('second', now()) -- identifiers removed +SELECT extract('second', _()) -- identifiers removed parse SELECT TRIM('xy' from 'xyxtrimyyx') diff --git a/pkg/sql/parser/testdata/reserved_keywords b/pkg/sql/parser/testdata/reserved_keywords index a0e1dc524138..7b8264178072 100644 --- a/pkg/sql/parser/testdata/reserved_keywords +++ b/pkg/sql/parser/testdata/reserved_keywords @@ -12,7 +12,7 @@ SELECT nothing(nothing + 1) AS nothing SELECT nothing(nothing + 1) AS nothing SELECT (nothing(((nothing) + (1)))) AS nothing -- fully parenthesized SELECT nothing(nothing + _) AS nothing -- literals removed -SELECT nothing(_ + 1) AS _ -- identifiers removed +SELECT _(_ + 1) AS _ -- identifiers removed parse SET a = nothing @@ -94,7 +94,7 @@ SELECT index(index), index(index), index.index(index) SELECT index(index), index(index), index.index(index) SELECT (index((index))), (index((index))), (index.index((index))) -- fully parenthesized SELECT index(index), index(index), index.index(index) -- literals removed -SELECT index(_), index(_), index.index(_) -- identifiers removed +SELECT _(_), _(_), _._(_) -- identifiers removed parse SELECT index index FROM a diff --git a/pkg/sql/parser/testdata/select_clauses b/pkg/sql/parser/testdata/select_clauses index 883a91196d7d..c8819454cdf5 100644 --- a/pkg/sql/parser/testdata/select_clauses +++ b/pkg/sql/parser/testdata/select_clauses @@ -796,7 +796,7 @@ SELECT a FROM ROWS FROM (a(x), b(y), c(z)) SELECT a FROM ROWS FROM (a(x), b(y), c(z)) SELECT (a) FROM ROWS FROM ((a((x))), (b((y))), (c((z)))) -- fully parenthesized SELECT a FROM ROWS FROM (a(x), b(y), c(z)) -- literals removed -SELECT _ FROM ROWS FROM (a(_), b(_), c(_)) -- identifiers removed +SELECT _ FROM ROWS FROM (_(_), _(_), _(_)) -- identifiers removed parse SELECT a FROM generate_series(1, 32) @@ -804,7 +804,7 @@ SELECT a FROM generate_series(1, 32) SELECT a FROM ROWS FROM (generate_series(1, 32)) -- normalized! SELECT (a) FROM ROWS FROM ((generate_series((1), (32)))) -- fully parenthesized SELECT a FROM ROWS FROM (generate_series(_, _)) -- literals removed -SELECT _ FROM ROWS FROM (generate_series(1, 32)) -- identifiers removed +SELECT _ FROM ROWS FROM (_(1, 32)) -- identifiers removed parse SELECT a FROM generate_series(1, 32) AS s (x) @@ -812,7 +812,7 @@ SELECT a FROM generate_series(1, 32) AS s (x) SELECT a FROM ROWS FROM (generate_series(1, 32)) AS s (x) -- normalized! SELECT (a) FROM ROWS FROM ((generate_series((1), (32)))) AS s (x) -- fully parenthesized SELECT a FROM ROWS FROM (generate_series(_, _)) AS s (x) -- literals removed -SELECT _ FROM ROWS FROM (generate_series(1, 32)) AS _ (_) -- identifiers removed +SELECT _ FROM ROWS FROM (_(1, 32)) AS _ (_) -- identifiers removed parse SELECT a FROM generate_series(1, 32) WITH ORDINALITY AS s (x) @@ -820,7 +820,7 @@ SELECT a FROM generate_series(1, 32) WITH ORDINALITY AS s (x) SELECT a FROM ROWS FROM (generate_series(1, 32)) WITH ORDINALITY AS s (x) -- normalized! SELECT (a) FROM ROWS FROM ((generate_series((1), (32)))) WITH ORDINALITY AS s (x) -- fully parenthesized SELECT a FROM ROWS FROM (generate_series(_, _)) WITH ORDINALITY AS s (x) -- literals removed -SELECT _ FROM ROWS FROM (generate_series(1, 32)) WITH ORDINALITY AS _ (_) -- identifiers removed +SELECT _ FROM ROWS FROM (_(1, 32)) WITH ORDINALITY AS _ (_) -- identifiers removed parse @@ -845,7 +845,7 @@ SELECT a FROM t1, LATERAL ROWS FROM (generate_series(1, t1.x)) SELECT a FROM t1, LATERAL ROWS FROM (generate_series(1, t1.x)) SELECT (a) FROM t1, LATERAL ROWS FROM ((generate_series((1), (t1.x)))) -- fully parenthesized SELECT a FROM t1, LATERAL ROWS FROM (generate_series(_, t1.x)) -- literals removed -SELECT _ FROM _, LATERAL ROWS FROM (generate_series(1, _._)) -- identifiers removed +SELECT _ FROM _, LATERAL ROWS FROM (_(1, _._)) -- identifiers removed parse SELECT a FROM LATERAL generate_series(1, 32) @@ -853,7 +853,7 @@ SELECT a FROM LATERAL generate_series(1, 32) SELECT a FROM LATERAL ROWS FROM (generate_series(1, 32)) -- normalized! SELECT (a) FROM LATERAL ROWS FROM ((generate_series((1), (32)))) -- fully parenthesized SELECT a FROM LATERAL ROWS FROM (generate_series(_, _)) -- literals removed -SELECT _ FROM LATERAL ROWS FROM (generate_series(1, 32)) -- identifiers removed +SELECT _ FROM LATERAL ROWS FROM (_(1, 32)) -- identifiers removed parse @@ -894,7 +894,7 @@ SELECT count(DISTINCT a) FROM t SELECT count(DISTINCT a) FROM t SELECT (count(DISTINCT (a))) FROM t -- fully parenthesized SELECT count(DISTINCT a) FROM t -- literals removed -SELECT count(DISTINCT _) FROM _ -- identifiers removed +SELECT _(DISTINCT _) FROM _ -- identifiers removed parse SELECT count(ALL a) FROM t @@ -902,7 +902,7 @@ SELECT count(ALL a) FROM t SELECT count(ALL a) FROM t SELECT (count(ALL (a))) FROM t -- fully parenthesized SELECT count(ALL a) FROM t -- literals removed -SELECT count(ALL _) FROM _ -- identifiers removed +SELECT _(ALL _) FROM _ -- identifiers removed parse SELECT a FROM t WHERE a = b @@ -1383,7 +1383,7 @@ SELECT a FROM t WHERE a = b() SELECT a FROM t WHERE a = b() SELECT (a) FROM t WHERE ((a) = (b())) -- fully parenthesized SELECT a FROM t WHERE a = b() -- literals removed -SELECT _ FROM _ WHERE _ = b() -- identifiers removed +SELECT _ FROM _ WHERE _ = _() -- identifiers removed parse SELECT a FROM t WHERE a = b(c) @@ -1391,7 +1391,7 @@ SELECT a FROM t WHERE a = b(c) SELECT a FROM t WHERE a = b(c) SELECT (a) FROM t WHERE ((a) = (b((c)))) -- fully parenthesized SELECT a FROM t WHERE a = b(c) -- literals removed -SELECT _ FROM _ WHERE _ = b(_) -- identifiers removed +SELECT _ FROM _ WHERE _ = _(_) -- identifiers removed parse SELECT a FROM t WHERE a = b(c, d) @@ -1399,7 +1399,7 @@ SELECT a FROM t WHERE a = b(c, d) SELECT a FROM t WHERE a = b(c, d) SELECT (a) FROM t WHERE ((a) = (b((c), (d)))) -- fully parenthesized SELECT a FROM t WHERE a = b(c, d) -- literals removed -SELECT _ FROM _ WHERE _ = b(_, _) -- identifiers removed +SELECT _ FROM _ WHERE _ = _(_, _) -- identifiers removed parse SELECT a FROM t WHERE a = count(*) @@ -1407,7 +1407,7 @@ SELECT a FROM t WHERE a = count(*) SELECT a FROM t WHERE a = count(*) SELECT (a) FROM t WHERE ((a) = (count((*)))) -- fully parenthesized SELECT a FROM t WHERE a = count(*) -- literals removed -SELECT _ FROM _ WHERE _ = count(*) -- identifiers removed +SELECT _ FROM _ WHERE _ = _(*) -- identifiers removed parse SELECT a FROM t WHERE a = IF(b, c, d) @@ -1640,7 +1640,7 @@ SELECT sum(x ORDER BY y) FROM t SELECT sum(x ORDER BY y) FROM t SELECT (sum((x) ORDER BY (y))) FROM t -- fully parenthesized SELECT sum(x ORDER BY y) FROM t -- literals removed -SELECT sum(_ ORDER BY _) FROM _ -- identifiers removed +SELECT _(_ ORDER BY _) FROM _ -- identifiers removed parse SELECT sum(x ORDER BY y, z) FROM t @@ -1648,7 +1648,7 @@ SELECT sum(x ORDER BY y, z) FROM t SELECT sum(x ORDER BY y, z) FROM t SELECT (sum((x) ORDER BY (y), (z))) FROM t -- fully parenthesized SELECT sum(x ORDER BY y, z) FROM t -- literals removed -SELECT sum(_ ORDER BY _, _) FROM _ -- identifiers removed +SELECT _(_ ORDER BY _, _) FROM _ -- identifiers removed parse SELECT a FROM t HAVING a = b @@ -1720,7 +1720,7 @@ SELECT avg(1) OVER w FROM t SELECT avg(1) OVER w FROM t SELECT (avg((1)) OVER w) FROM t -- fully parenthesized SELECT avg(_) OVER w FROM t -- literals removed -SELECT avg(1) OVER _ FROM _ -- identifiers removed +SELECT _(1) OVER _ FROM _ -- identifiers removed parse SELECT avg(1) OVER () FROM t @@ -1728,7 +1728,7 @@ SELECT avg(1) OVER () FROM t SELECT avg(1) OVER () FROM t SELECT (avg((1)) OVER ()) FROM t -- fully parenthesized SELECT avg(_) OVER () FROM t -- literals removed -SELECT avg(1) OVER () FROM _ -- identifiers removed +SELECT _(1) OVER () FROM _ -- identifiers removed parse SELECT avg(1) OVER (w) FROM t @@ -1736,7 +1736,7 @@ SELECT avg(1) OVER (w) FROM t SELECT avg(1) OVER (w) FROM t SELECT (avg((1)) OVER (w)) FROM t -- fully parenthesized SELECT avg(_) OVER (w) FROM t -- literals removed -SELECT avg(1) OVER (_) FROM _ -- identifiers removed +SELECT _(1) OVER (_) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b) FROM t @@ -1744,7 +1744,7 @@ SELECT avg(1) OVER (PARTITION BY b) FROM t SELECT avg(1) OVER (PARTITION BY b) FROM t SELECT (avg((1)) OVER (PARTITION BY (b))) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ORDER BY c) FROM t @@ -1752,7 +1752,7 @@ SELECT avg(1) OVER (ORDER BY c) FROM t SELECT avg(1) OVER (ORDER BY c) FROM t SELECT (avg((1)) OVER (ORDER BY (c))) FROM t -- fully parenthesized SELECT avg(_) OVER (ORDER BY c) FROM t -- literals removed -SELECT avg(1) OVER (ORDER BY _) FROM _ -- identifiers removed +SELECT _(1) OVER (ORDER BY _) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b ORDER BY c) FROM t @@ -1760,7 +1760,7 @@ SELECT avg(1) OVER (PARTITION BY b ORDER BY c) FROM t SELECT avg(1) OVER (PARTITION BY b ORDER BY c) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) ORDER BY (c))) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b ORDER BY c) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ ORDER BY _) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ ORDER BY _) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w PARTITION BY b ORDER BY c) FROM t @@ -1768,7 +1768,7 @@ SELECT avg(1) OVER (w PARTITION BY b ORDER BY c) FROM t SELECT avg(1) OVER (w PARTITION BY b ORDER BY c) FROM t SELECT (avg((1)) OVER (w PARTITION BY (b) ORDER BY (c))) FROM t -- fully parenthesized SELECT avg(_) OVER (w PARTITION BY b ORDER BY c) FROM t -- literals removed -SELECT avg(1) OVER (_ PARTITION BY _ ORDER BY _) FROM _ -- identifiers removed +SELECT _(1) OVER (_ PARTITION BY _ ORDER BY _) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING) FROM t @@ -1776,7 +1776,7 @@ SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (ROWS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS 1 PRECEDING) FROM t @@ -1784,7 +1784,7 @@ SELECT avg(1) OVER (ROWS 1 PRECEDING) FROM t SELECT avg(1) OVER (ROWS 1 PRECEDING) FROM t SELECT (avg((1)) OVER (ROWS (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS CURRENT ROW) FROM t @@ -1792,7 +1792,7 @@ SELECT avg(1) OVER (ROWS CURRENT ROW) FROM t SELECT avg(1) OVER (ROWS CURRENT ROW) FROM t SELECT (avg((1)) OVER (ROWS CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (ROWS CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t @@ -1800,7 +1800,7 @@ SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t @@ -1808,7 +1808,7 @@ SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t @@ -1816,7 +1816,7 @@ SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t @@ -1824,7 +1824,7 @@ SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FR SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t @@ -1832,7 +1832,7 @@ SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN (1) PRECEDING AND (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN _ PRECEDING AND _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t @@ -1840,7 +1840,7 @@ SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN (1) PRECEDING AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN _ PRECEDING AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t @@ -1848,7 +1848,7 @@ SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN (1) PRECEDING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN _ PRECEDING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t @@ -1856,7 +1856,7 @@ SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN (1) PRECEDING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN _ PRECEDING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t @@ -1864,7 +1864,7 @@ SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t @@ -1872,7 +1872,7 @@ SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN CURRENT ROW AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN CURRENT ROW AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t @@ -1880,7 +1880,7 @@ SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t @@ -1888,7 +1888,7 @@ SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN (1) FOLLOWING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN _ FOLLOWING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t @@ -1896,7 +1896,7 @@ SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (ROWS BETWEEN (1) FOLLOWING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS BETWEEN _ FOLLOWING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w ROWS UNBOUNDED PRECEDING) FROM t @@ -1904,7 +1904,7 @@ SELECT avg(1) OVER (w ROWS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (w ROWS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (w ROWS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (w ROWS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (_ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (_ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b ROWS UNBOUNDED PRECEDING) FROM t @@ -1912,7 +1912,7 @@ SELECT avg(1) OVER (PARTITION BY b ROWS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (PARTITION BY b ROWS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) ROWS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b ROWS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t @@ -1920,7 +1920,7 @@ SELECT avg(1) OVER (ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (ORDER BY (c) ROWS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ORDER BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ORDER BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t @@ -1928,7 +1928,7 @@ SELECT avg(1) OVER (PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) ORDER BY (c) ROWS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ ORDER BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ ORDER BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t @@ -1936,7 +1936,7 @@ SELECT avg(1) OVER (w PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (w PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (w PARTITION BY (b) ORDER BY (c) ROWS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (w PARTITION BY b ORDER BY c ROWS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (_ PARTITION BY _ ORDER BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (_ PARTITION BY _ ORDER BY _ ROWS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE UNBOUNDED PRECEDING) FROM t @@ -1944,7 +1944,7 @@ SELECT avg(1) OVER (RANGE UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (RANGE UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (RANGE UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE 1 PRECEDING) FROM t @@ -1952,7 +1952,7 @@ SELECT avg(1) OVER (RANGE 1 PRECEDING) FROM t SELECT avg(1) OVER (RANGE 1 PRECEDING) FROM t SELECT (avg((1)) OVER (RANGE (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE CURRENT ROW) FROM t @@ -1960,7 +1960,7 @@ SELECT avg(1) OVER (RANGE CURRENT ROW) FROM t SELECT avg(1) OVER (RANGE CURRENT ROW) FROM t SELECT (avg((1)) OVER (RANGE CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (RANGE CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t @@ -1968,7 +1968,7 @@ SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t @@ -1976,7 +1976,7 @@ SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t @@ -1984,7 +1984,7 @@ SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t @@ -1992,7 +1992,7 @@ SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) F SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t @@ -2000,7 +2000,7 @@ SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN (1) PRECEDING AND (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN _ PRECEDING AND _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t @@ -2008,7 +2008,7 @@ SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN (1) PRECEDING AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN _ PRECEDING AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t @@ -2016,7 +2016,7 @@ SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN (1) PRECEDING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN _ PRECEDING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t @@ -2024,7 +2024,7 @@ SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN (1) PRECEDING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN _ PRECEDING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) FROM t @@ -2032,7 +2032,7 @@ SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) FROM t SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t @@ -2040,7 +2040,7 @@ SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN CURRENT ROW AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN CURRENT ROW AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t @@ -2048,7 +2048,7 @@ SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t @@ -2056,7 +2056,7 @@ SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN (1) FOLLOWING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN _ FOLLOWING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t @@ -2064,7 +2064,7 @@ SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (RANGE BETWEEN (1) FOLLOWING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (RANGE BETWEEN _ FOLLOWING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (RANGE BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w RANGE UNBOUNDED PRECEDING) FROM t @@ -2072,7 +2072,7 @@ SELECT avg(1) OVER (w RANGE UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (w RANGE UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (w RANGE UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (w RANGE UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (_ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (_ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b RANGE UNBOUNDED PRECEDING) FROM t @@ -2080,7 +2080,7 @@ SELECT avg(1) OVER (PARTITION BY b RANGE UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (PARTITION BY b RANGE UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) RANGE UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b RANGE UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t @@ -2088,7 +2088,7 @@ SELECT avg(1) OVER (ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (ORDER BY (c) RANGE UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ORDER BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ORDER BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t @@ -2096,7 +2096,7 @@ SELECT avg(1) OVER (PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) ORDER BY (c) RANGE UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ ORDER BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ ORDER BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t @@ -2104,7 +2104,7 @@ SELECT avg(1) OVER (w PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM SELECT avg(1) OVER (w PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (w PARTITION BY (b) ORDER BY (c) RANGE UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (w PARTITION BY b ORDER BY c RANGE UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (_ PARTITION BY _ ORDER BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (_ PARTITION BY _ ORDER BY _ RANGE UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS UNBOUNDED PRECEDING) FROM t @@ -2112,7 +2112,7 @@ SELECT avg(1) OVER (GROUPS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (GROUPS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (GROUPS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS 1 PRECEDING) FROM t @@ -2120,7 +2120,7 @@ SELECT avg(1) OVER (GROUPS 1 PRECEDING) FROM t SELECT avg(1) OVER (GROUPS 1 PRECEDING) FROM t SELECT (avg((1)) OVER (GROUPS (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS CURRENT ROW) FROM t @@ -2128,7 +2128,7 @@ SELECT avg(1) OVER (GROUPS CURRENT ROW) FROM t SELECT avg(1) OVER (GROUPS CURRENT ROW) FROM t SELECT (avg((1)) OVER (GROUPS CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t @@ -2136,7 +2136,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t @@ -2144,7 +2144,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t @@ -2152,7 +2152,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t @@ -2160,7 +2160,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t @@ -2168,7 +2168,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN (1) PRECEDING AND (1) PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN _ PRECEDING AND _ PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t @@ -2176,7 +2176,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN (1) PRECEDING AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN _ PRECEDING AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t @@ -2184,7 +2184,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN (1) PRECEDING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN _ PRECEDING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t @@ -2192,7 +2192,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN (1) PRECEDING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN _ PRECEDING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t @@ -2200,7 +2200,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t @@ -2208,7 +2208,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN CURRENT ROW AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN CURRENT ROW AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t @@ -2216,7 +2216,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t @@ -2224,7 +2224,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN (1) FOLLOWING AND (1) FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN _ FOLLOWING AND _ FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t @@ -2232,7 +2232,7 @@ SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM t SELECT (avg((1)) OVER (GROUPS BETWEEN (1) FOLLOWING AND UNBOUNDED FOLLOWING)) FROM t -- fully parenthesized SELECT avg(_) OVER (GROUPS BETWEEN _ FOLLOWING AND UNBOUNDED FOLLOWING) FROM t -- literals removed -SELECT avg(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed +SELECT _(1) OVER (GROUPS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w GROUPS UNBOUNDED PRECEDING) FROM t @@ -2240,7 +2240,7 @@ SELECT avg(1) OVER (w GROUPS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (w GROUPS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (w GROUPS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (w GROUPS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (_ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (_ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b GROUPS UNBOUNDED PRECEDING) FROM t @@ -2248,7 +2248,7 @@ SELECT avg(1) OVER (PARTITION BY b GROUPS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (PARTITION BY b GROUPS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) GROUPS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b GROUPS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t @@ -2256,7 +2256,7 @@ SELECT avg(1) OVER (ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (ORDER BY (c) GROUPS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (ORDER BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (ORDER BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t @@ -2264,7 +2264,7 @@ SELECT avg(1) OVER (PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t SELECT avg(1) OVER (PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (PARTITION BY (b) ORDER BY (c) GROUPS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (PARTITION BY _ ORDER BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (PARTITION BY _ ORDER BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (w PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t @@ -2272,7 +2272,7 @@ SELECT avg(1) OVER (w PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM SELECT avg(1) OVER (w PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t SELECT (avg((1)) OVER (w PARTITION BY (b) ORDER BY (c) GROUPS UNBOUNDED PRECEDING)) FROM t -- fully parenthesized SELECT avg(_) OVER (w PARTITION BY b ORDER BY c GROUPS UNBOUNDED PRECEDING) FROM t -- literals removed -SELECT avg(1) OVER (_ PARTITION BY _ ORDER BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed +SELECT _(1) OVER (_ PARTITION BY _ ORDER BY _ GROUPS UNBOUNDED PRECEDING) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM t @@ -2280,7 +2280,7 @@ SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM t SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM t SELECT (avg((1)) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM t -- literals removed -SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP) FROM t @@ -2288,7 +2288,7 @@ SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP) FROM t SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP) FROM t SELECT (avg((1)) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP) FROM t -- literals removed -SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE GROUP) FROM _ -- identifiers removed parse SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES) FROM t @@ -2296,7 +2296,7 @@ SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES) FROM t SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES) FROM t SELECT (avg((1)) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES)) FROM t -- fully parenthesized SELECT avg(_) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES) FROM t -- literals removed -SELECT avg(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES) FROM _ -- identifiers removed +SELECT _(1) OVER (ROWS UNBOUNDED PRECEDING EXCLUDE TIES) FROM _ -- identifiers removed error SELECT avg(1) OVER (ROWS UNBOUNDED FOLLOWING) FROM t @@ -2360,7 +2360,7 @@ SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY c) FROM t SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY c) FROM t SELECT (percentile_disc((0.50)) WITHIN GROUP (ORDER BY (c))) FROM t -- fully parenthesized SELECT percentile_disc(_) WITHIN GROUP (ORDER BY c) FROM t -- literals removed -SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed +SELECT _(0.50) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed parse SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY c DESC) FROM t @@ -2368,7 +2368,7 @@ SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY c DESC) FROM t SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY c DESC) FROM t SELECT (percentile_disc((0.50)) WITHIN GROUP (ORDER BY (c) DESC)) FROM t -- fully parenthesized SELECT percentile_disc(_) WITHIN GROUP (ORDER BY c DESC) FROM t -- literals removed -SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY _ DESC) FROM _ -- identifiers removed +SELECT _(0.50) WITHIN GROUP (ORDER BY _ DESC) FROM _ -- identifiers removed parse SELECT percentile_cont(0.50) WITHIN GROUP (ORDER BY c) FROM t @@ -2376,7 +2376,7 @@ SELECT percentile_cont(0.50) WITHIN GROUP (ORDER BY c) FROM t SELECT percentile_cont(0.50) WITHIN GROUP (ORDER BY c) FROM t SELECT (percentile_cont((0.50)) WITHIN GROUP (ORDER BY (c))) FROM t -- fully parenthesized SELECT percentile_cont(_) WITHIN GROUP (ORDER BY c) FROM t -- literals removed -SELECT percentile_cont(0.50) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed +SELECT _(0.50) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed parse SELECT percentile_disc(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY c) FROM t @@ -2384,7 +2384,7 @@ SELECT percentile_disc(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY c) FROM t SELECT percentile_disc(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY c) FROM t SELECT (percentile_disc((ARRAY[(0.95), (0.90)])) WITHIN GROUP (ORDER BY (c))) FROM t -- fully parenthesized SELECT percentile_disc(ARRAY[_, _]) WITHIN GROUP (ORDER BY c) FROM t -- literals removed -SELECT percentile_disc(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed +SELECT _(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed parse SELECT percentile_cont(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY c) FROM t @@ -2392,7 +2392,7 @@ SELECT percentile_cont(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY c) FROM t SELECT percentile_cont(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY c) FROM t SELECT (percentile_cont((ARRAY[(0.95), (0.90)])) WITHIN GROUP (ORDER BY (c))) FROM t -- fully parenthesized SELECT percentile_cont(ARRAY[_, _]) WITHIN GROUP (ORDER BY c) FROM t -- literals removed -SELECT percentile_cont(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed +SELECT _(ARRAY[0.95, 0.90]) WITHIN GROUP (ORDER BY _) FROM _ -- identifiers removed error SELECT percentile_disc(0.50) WITHIN GROUP (ORDER BY f, s) FROM x; @@ -2408,7 +2408,7 @@ SELECT avg(1) FILTER (WHERE a > b) SELECT avg(1) FILTER (WHERE a > b) SELECT (avg((1)) FILTER (WHERE ((a) > (b)))) -- fully parenthesized SELECT avg(_) FILTER (WHERE a > b) -- literals removed -SELECT avg(1) FILTER (WHERE _ > _) -- identifiers removed +SELECT _(1) FILTER (WHERE _ > _) -- identifiers removed parse SELECT avg(1) FILTER (WHERE a > b) OVER (ORDER BY c) @@ -2416,7 +2416,7 @@ SELECT avg(1) FILTER (WHERE a > b) OVER (ORDER BY c) SELECT avg(1) FILTER (WHERE a > b) OVER (ORDER BY c) SELECT (avg((1)) FILTER (WHERE ((a) > (b))) OVER (ORDER BY (c))) -- fully parenthesized SELECT avg(_) FILTER (WHERE a > b) OVER (ORDER BY c) -- literals removed -SELECT avg(1) FILTER (WHERE _ > _) OVER (ORDER BY _) -- identifiers removed +SELECT _(1) FILTER (WHERE _ > _) OVER (ORDER BY _) -- identifiers removed parse SELECT a FROM t UNION SELECT 1 FROM t @@ -3050,7 +3050,7 @@ SELECT * FROM json_to_record('') AS t(a INT, b TEXT, c foo) SELECT * FROM ROWS FROM (json_to_record('')) AS t (a INT8, b STRING, c foo) -- normalized! SELECT (*) FROM ROWS FROM ((json_to_record(('')))) AS t (a INT8, b STRING, c foo) -- fully parenthesized SELECT * FROM ROWS FROM (json_to_record('_')) AS t (a INT8, b STRING, c foo) -- literals removed -SELECT * FROM ROWS FROM (json_to_record('')) AS _ (_ INT8, _ STRING, _ _) -- identifiers removed +SELECT * FROM ROWS FROM (_('')) AS _ (_ INT8, _ STRING, _ _) -- identifiers removed parse SELECT substring('stringstringstring',1,10) QUERY @@ -3068,7 +3068,7 @@ SELECT * FROM json_to_record('') AS t(a "Nice Enum 📙", b TEXT, c foo) SELECT * FROM ROWS FROM (json_to_record('')) AS t (a "Nice Enum 📙", b STRING, c foo) -- normalized! SELECT (*) FROM ROWS FROM ((json_to_record(('')))) AS t (a "Nice Enum 📙", b STRING, c foo) -- fully parenthesized SELECT * FROM ROWS FROM (json_to_record('_')) AS t (a "Nice Enum 📙", b STRING, c foo) -- literals removed -SELECT * FROM ROWS FROM (json_to_record('')) AS _ (_ _, _ STRING, _ _) -- identifiers removed +SELECT * FROM ROWS FROM (_('')) AS _ (_ _, _ STRING, _ _) -- identifiers removed parse SELECT 123 AS OF FROM t diff --git a/pkg/sql/parser/testdata/select_exprs b/pkg/sql/parser/testdata/select_exprs index f8521543536c..c4cb7b04270b 100644 --- a/pkg/sql/parser/testdata/select_exprs +++ b/pkg/sql/parser/testdata/select_exprs @@ -110,7 +110,7 @@ SELECT~~+~++~bd(*) SELECT ~(~(~(~bd(*)))) -- normalized! SELECT (~((~((~((~(bd((*)))))))))) -- fully parenthesized SELECT ~(~(~(~bd(*)))) -- literals removed -SELECT ~(~(~(~bd(*)))) -- identifiers removed +SELECT ~(~(~(~_(*)))) -- identifiers removed parse SELECT true AND false @@ -198,7 +198,7 @@ SELECT FAMILY (x) SELECT family(x) -- normalized! SELECT (family((x))) -- fully parenthesized SELECT family(x) -- literals removed -SELECT family(_) -- identifiers removed +SELECT _(_) -- identifiers removed parse SELECT 1 IN (b) @@ -1937,7 +1937,7 @@ TIMESTAMP '2000-01-01 01:30:00'); SELECT overlaps(TIMESTAMP '2000-01-01 00:00:00', TIMESTAMP '2000-01-01 01:00:00', TIMESTAMP '2000-01-01 00:30:00', TIMESTAMP '2000-01-01 01:30:00') -- normalized! SELECT (overlaps((TIMESTAMP ('2000-01-01 00:00:00')), (TIMESTAMP ('2000-01-01 01:00:00')), (TIMESTAMP ('2000-01-01 00:30:00')), (TIMESTAMP ('2000-01-01 01:30:00')))) -- fully parenthesized SELECT overlaps(TIMESTAMP '_', TIMESTAMP '_', TIMESTAMP '_', TIMESTAMP '_') -- literals removed -SELECT overlaps(TIMESTAMP '2000-01-01 00:00:00', TIMESTAMP '2000-01-01 01:00:00', TIMESTAMP '2000-01-01 00:30:00', TIMESTAMP '2000-01-01 01:30:00') -- identifiers removed +SELECT _(TIMESTAMP '2000-01-01 00:00:00', TIMESTAMP '2000-01-01 01:00:00', TIMESTAMP '2000-01-01 00:30:00', TIMESTAMP '2000-01-01 01:30:00') -- identifiers removed parse SELECT @@ -2045,4 +2045,4 @@ SELECT my_func('a', 1, true) SELECT my_func('a', 1, true) SELECT (my_func(('a'), (1), (true))) -- fully parenthesized SELECT my_func('_', _, _) -- literals removed -SELECT my_func('a', 1, true) -- identifiers removed +SELECT _('a', 1, true) -- identifiers removed diff --git a/pkg/sql/parser/testdata/user_defined_functions b/pkg/sql/parser/testdata/user_defined_functions new file mode 100644 index 000000000000..479a9bd1b567 --- /dev/null +++ b/pkg/sql/parser/testdata/user_defined_functions @@ -0,0 +1,40 @@ +parse +SELECT udf() +---- +SELECT udf() +SELECT (udf()) -- fully parenthesized +SELECT udf() -- literals removed +SELECT _() -- identifiers removed + +parse +SELECT udf('arg1', 2.0, now(), rtrim('abcd')) +---- +SELECT udf('arg1', 2.0, now(), rtrim('abcd')) +SELECT (udf(('arg1'), (2.0), (now()), (rtrim(('abcd'))))) -- fully parenthesized +SELECT udf('_', _, now(), rtrim('_')) -- literals removed +SELECT _('arg1', 2.0, _(), _('abcd')) -- identifiers removed + +parse +SELECT floor(udf('arg')) +---- +SELECT floor(udf('arg')) +SELECT (floor((udf(('arg'))))) -- fully parenthesized +SELECT floor(udf('_')) -- literals removed +SELECT _(_('arg')) -- identifiers removed + +parse +SELECT substring(udf(1, 2), 3) +---- +SELECT substring(udf(1, 2), 3) +SELECT (substring((udf((1), (2))), (3))) -- fully parenthesized +SELECT substring(udf(_, _), _) -- literals removed +SELECT substring(_(1, 2), 3) -- identifiers removed + +# This is not a signature for the built-in function btrim. +parse +SELECT btrim() +---- +SELECT btrim() +SELECT (btrim()) -- fully parenthesized +SELECT btrim() -- literals removed +SELECT _() -- identifiers removed diff --git a/pkg/sql/schemachanger/scplan/testdata/alter_table_add_check_udf b/pkg/sql/schemachanger/scplan/testdata/alter_table_add_check_udf index 7e216f2b6ada..9848e7e3adb2 100644 --- a/pkg/sql/schemachanger/scplan/testdata/alter_table_add_check_udf +++ b/pkg/sql/schemachanger/scplan/testdata/alter_table_add_check_udf @@ -72,7 +72,7 @@ PreCommitPhase stage 2 of 2 with 6 MutationType ops RunningStatus: PostCommitPhase stage 1 of 2 with 1 ValidationType op pending Statements: - statement: ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1) - redactedstatement: ALTER TABLE ‹defaultdb›.public.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›) + redactedstatement: ALTER TABLE ‹defaultdb›.public.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›) statementtag: ALTER TABLE PostCommitPhase stage 1 of 2 with 1 ValidationType op transitions: diff --git a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain index 45e6aa223d38..d57f5f6aa19c 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain @@ -5,7 +5,7 @@ CREATE FUNCTION f(b INT) RETURNS INT LANGUAGE SQL AS $$ SELECT b + 1 $$; /* test */ EXPLAIN (DDL) ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1); ---- -Schema change plan for ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›); +Schema change plan for ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›); ├── StatementPhase │ └── Stage 1 of 1 in StatementPhase │ ├── 2 elements transitioning toward PUBLIC diff --git a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain_shape b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain_shape index b37a008d882a..99b49d9bb069 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain_shape +++ b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.explain_shape @@ -5,7 +5,7 @@ CREATE FUNCTION f(b INT) RETURNS INT LANGUAGE SQL AS $$ SELECT b + 1 $$; /* test */ EXPLAIN (DDL, SHAPE) ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1); ---- -Schema change plan for ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›); +Schema change plan for ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›); ├── execute 1 system table mutations transaction ├── validate non-index-backed constraint check_b+ in relation t └── execute 1 system table mutations transaction diff --git a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects index d12605157d38..a7046c01b356 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects @@ -17,7 +17,7 @@ write *eventpb.AlterTable to event log: mutationId: 1 sql: descriptorId: 104 - statement: ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›) + statement: ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›) tag: ALTER TABLE user: root tableName: defaultdb.public.t @@ -117,7 +117,7 @@ upsert descriptor #104 + name: t + relevantStatements: + - statement: - + redactedStatement: ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›) + + redactedStatement: ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›) + statement: ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1) + statementTag: ALTER TABLE + revertible: true @@ -222,7 +222,7 @@ upsert descriptor #104 - name: t - relevantStatements: - - statement: - - redactedStatement: ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›) + - redactedStatement: ALTER TABLE ‹defaultdb›.‹public›.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›) - statement: ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1) - statementTag: ALTER TABLE - revertible: true diff --git a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_1_of_2.explain b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_1_of_2.explain index 0ab310261a38..7c389971ee40 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_1_of_2.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_1_of_2.explain @@ -6,7 +6,7 @@ CREATE FUNCTION f(b INT) RETURNS INT LANGUAGE SQL AS $$ SELECT b + 1 $$; ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1); EXPLAIN (DDL) rollback at post-commit stage 1 of 2; ---- -Schema change plan for rolling back ALTER TABLE ‹defaultdb›.public.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›); +Schema change plan for rolling back ALTER TABLE ‹defaultdb›.public.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›); └── PostCommitNonRevertiblePhase └── Stage 1 of 1 in PostCommitNonRevertiblePhase ├── 2 elements transitioning toward ABSENT diff --git a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_2_of_2.explain b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_2_of_2.explain index 4d394f590845..e01415c1262c 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_2_of_2.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf__rollback_2_of_2.explain @@ -6,7 +6,7 @@ CREATE FUNCTION f(b INT) RETURNS INT LANGUAGE SQL AS $$ SELECT b + 1 $$; ALTER TABLE t ADD CONSTRAINT check_b CHECK (f(b) > 1); EXPLAIN (DDL) rollback at post-commit stage 2 of 2; ---- -Schema change plan for rolling back ALTER TABLE ‹defaultdb›.public.‹t› ADD CONSTRAINT ‹check_b› CHECK (f(‹b›) > ‹1›); +Schema change plan for rolling back ALTER TABLE ‹defaultdb›.public.‹t› ADD CONSTRAINT ‹check_b› CHECK (‹f›(‹b›) > ‹1›); └── PostCommitNonRevertiblePhase └── Stage 1 of 1 in PostCommitNonRevertiblePhase ├── 2 elements transitioning toward ABSENT diff --git a/pkg/sql/sem/tree/expr.go b/pkg/sql/sem/tree/expr.go index 2324b4b2453b..33b63889b882 100644 --- a/pkg/sql/sem/tree/expr.go +++ b/pkg/sql/sem/tree/expr.go @@ -1377,11 +1377,13 @@ func (node *FuncExpr) Format(ctx *FmtCtx) { typ = funcTypeName[node.Type] + " " } - // We need to remove name anonymization/redaction for the function name in - // particular. Do this by overriding the flags. - // TODO(thomas): when function names are correctly typed as FunctionDefinition - // remove FmtMarkRedactionNode from being overridden. - ctx.WithFlags(ctx.flags&^FmtAnonymize&^FmtMarkRedactionNode|FmtBareIdentifiers, func() { + // We let anonymization and redaction flags pass through, which will cause + // built-in functions to be redacted if we have not resolved them. This is + // because we cannot distinguish between built-in functions and UDFs before + // they are resolved. We conservatively redact function names if requested. + // TODO(111385): Investigate ways to identify built-in functions before + // type-checking. + ctx.WithFlags(ctx.flags|FmtBareIdentifiers, func() { ctx.FormatNode(&node.Func) }) diff --git a/pkg/sql/sem/tree/format.go b/pkg/sql/sem/tree/format.go index 70237acca2d4..d4d807322abb 100644 --- a/pkg/sql/sem/tree/format.go +++ b/pkg/sql/sem/tree/format.go @@ -76,9 +76,7 @@ const ( // also shorten long lists in tuples, VALUES and array expressions. FmtHideConstants - // FmtAnonymize instructs the pretty-printer to remove - // any name but function names. - // TODO(knz): temporary until a better solution is found for #13968 + // FmtAnonymize instructs the pretty-printer to remove any name. FmtAnonymize // FmtAlwaysQualifyTableNames instructs the pretty-printer to diff --git a/pkg/sql/sem/tree/function_definition.go b/pkg/sql/sem/tree/function_definition.go index 268f26d2f845..ffc86e8d62ed 100644 --- a/pkg/sql/sem/tree/function_definition.go +++ b/pkg/sql/sem/tree/function_definition.go @@ -203,6 +203,8 @@ var OidToBuiltinName map[oid.Oid]string var OidToQualifiedBuiltinOverload map[oid.Oid]QualifiedOverload // Format implements the NodeFormatter interface. +// FunctionDefinitions should always be builtin functions, so we do not need to +// anonymize them, even if the flag is set. func (fd *FunctionDefinition) Format(ctx *FmtCtx) { ctx.WriteString(fd.Name) } @@ -211,6 +213,8 @@ func (fd *FunctionDefinition) Format(ctx *FmtCtx) { func (fd *FunctionDefinition) String() string { return AsString(fd) } // Format implements the NodeFormatter interface. +// ResolvedFunctionDefinitions should always be builtin functions, so we do not +// need to anonymize them, even if the flag is set. func (fd *ResolvedFunctionDefinition) Format(ctx *FmtCtx) { // This is necessary when deserializing function expressions for SHOW CREATE // statements. When deserializing a function expression with function OID diff --git a/pkg/sql/sem/tree/name_part.go b/pkg/sql/sem/tree/name_part.go index 7ac5a228b786..fffeefde0d21 100644 --- a/pkg/sql/sem/tree/name_part.go +++ b/pkg/sql/sem/tree/name_part.go @@ -55,13 +55,13 @@ func NameString(s string) string { // identifier suitable for printing in error messages, avoiding a heap // allocation. func ErrNameStringP(s *string) string { - return ErrString(((*Name)(s))) + return ErrString((*Name)(s)) } // ErrNameString escapes an identifier stored a string to a SQL // identifier suitable for printing in error messages. func ErrNameString(s string) string { - return ErrString(((*Name)(&s))) + return ErrString((*Name)(&s)) } // Normalize normalizes to lowercase and Unicode Normalization Form C @@ -178,6 +178,7 @@ type NameParts = [4]string // Format implements the NodeFormatter interface. func (u *UnresolvedName) Format(ctx *FmtCtx) { + isCrdbInternal := false stopAt := 1 if u.Star { stopAt = 2 @@ -188,9 +189,23 @@ func (u *UnresolvedName) Format(ctx *FmtCtx) { // the grammar, so print it out as a "Name". Every part after that is // necessarily an unrestricted name. if i == u.NumParts { - ctx.FormatNode((*Name)(&u.Parts[i-1])) + if u.Parts[i-1] == "crdb_internal" && i > 1 { + ctx.WithFlags(ctx.flags&^FmtAnonymize&^FmtMarkRedactionNode, func() { + ctx.FormatNode((*Name)(&u.Parts[i-1])) + }) + isCrdbInternal = true + } else { + ctx.FormatNode((*Name)(&u.Parts[i-1])) + } } else { - ctx.FormatNode((*UnrestrictedName)(&u.Parts[i-1])) + if isCrdbInternal { + ctx.WithFlags(ctx.flags&^FmtAnonymize&^FmtMarkRedactionNode, func() { + ctx.FormatNode((*UnrestrictedName)(&u.Parts[i-1])) + }) + } else { + ctx.FormatNode((*UnrestrictedName)(&u.Parts[i-1])) + + } } if i > 1 { ctx.WriteByte('.') diff --git a/pkg/sql/statement_mark_redaction_test.go b/pkg/sql/statement_mark_redaction_test.go index 3fd222b23440..27b6ab1da140 100644 --- a/pkg/sql/statement_mark_redaction_test.go +++ b/pkg/sql/statement_mark_redaction_test.go @@ -44,17 +44,9 @@ func TestMarkRedactionStatement(t *testing.T) { "CREATE TABLE defaultdb.public.t()", "CREATE TABLE ‹defaultdb›.public.‹t› ()", }, - // Note: this test only passes due to the overriding of the FmtMarkRedactionNode flag - // when formatting FuncExpr.Func. Although the overriding of the redaction flag - // is correct (function names do not hold PII and therefore do not need redaction), - // the incorrect typing of FuncExpr.Func as an UnresolvedName results in the - // function name being incorrectly redacted when logging statements. In this - // test case, that would be: SELECT ‹lower›(‹'foo'›). - // The intended functionality is for function names to be correctly typed as - // FunctionDefinition, which is logically identified as non-PII. { "SELECT lower('foo')", - "SELECT lower(‹'foo'›)", + "SELECT ‹lower›(‹'foo'›)", }, { "SELECT crdb_internal.node_executable_version()", From f4bb7665f12389a75c50771fc6584c2cd9bc8d05 Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Sat, 7 Oct 2023 00:25:17 +0100 Subject: [PATCH 5/7] sysutil: add IsAddrInUse Epic: none Release note: None --- pkg/util/sysutil/BUILD.bazel | 1 + pkg/util/sysutil/sysutil.go | 5 +++++ pkg/util/sysutil/sysutil_test.go | 11 +++++++++++ 3 files changed, 17 insertions(+) diff --git a/pkg/util/sysutil/BUILD.bazel b/pkg/util/sysutil/BUILD.bazel index d5b8fbfff80c..bbc1f5935e91 100644 --- a/pkg/util/sysutil/BUILD.bazel +++ b/pkg/util/sysutil/BUILD.bazel @@ -74,6 +74,7 @@ go_test( embed = [":sysutil"], deps = [ "@com_github_cockroachdb_errors//:errors", + "@com_github_stretchr_testify//require", ] + select({ "@io_bazel_rules_go//go/platform:aix": [ "@com_github_stretchr_testify//assert", diff --git a/pkg/util/sysutil/sysutil.go b/pkg/util/sysutil/sysutil.go index 12d462a72e30..67d05ba16eea 100644 --- a/pkg/util/sysutil/sysutil.go +++ b/pkg/util/sysutil/sysutil.go @@ -76,6 +76,11 @@ func IsErrTimedOut(err error) bool { return errors.Is(err, syscall.ETIMEDOUT) } +// IsAddrInUse returns true if an error is an EADDRINUSE error. +func IsAddrInUse(err error) bool { + return errors.Is(err, syscall.EADDRINUSE) +} + // InterruptSelf sends Interrupt to the process itself. func InterruptSelf() error { pr, err := os.FindProcess(os.Getpid()) diff --git a/pkg/util/sysutil/sysutil_test.go b/pkg/util/sysutil/sysutil_test.go index 1a4f7364d824..7b22310822b9 100644 --- a/pkg/util/sysutil/sysutil_test.go +++ b/pkg/util/sysutil/sysutil_test.go @@ -11,10 +11,12 @@ package sysutil import ( + "net" "os/exec" "testing" "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" ) func TestExitStatus(t *testing.T) { @@ -31,3 +33,12 @@ func TestExitStatus(t *testing.T) { t.Fatalf("expected exit status 42, but got %d", status) } } + +func TestIsAddrInUse(t *testing.T) { + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + defer func() { _ = ln.Close() }() + + _, err = net.Listen("tcp", ln.Addr().String()) + require.True(t, IsAddrInUse(err)) +} From 395f01850c5d6aebeb725d8691016de829a431b3 Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Mon, 2 Oct 2023 14:16:57 +0100 Subject: [PATCH 6/7] cli,server: allow user to specify port range for secondary VCs Previously, the user could set a port range offset that would instruct the tenant controller to choose a gRPC port starting at the KV server's RPC port + the offset + an index. While this provides the user some predictability, there is no upper bound on the port that could be picked. Here, we add a new option --internal-rpc-port-range That allows the user to specify an explicit range. Epic: none Release note (cluster virtualization): A new flag --internal-rpc-port range allows operators to specify the port range used by secondary virtual clusters for node-to-node communication. Users using the physical replication or virtual cluster preview features should use this flag if they require the cockroach processes to only communicate using ports in a known port range. --- pkg/base/addr_validation.go | 6 ++ pkg/base/config.go | 9 +++ pkg/base/test_server_args.go | 10 ++- pkg/cli/cliflags/flags.go | 32 +++++--- pkg/cli/democluster/demo_cluster.go | 15 ++-- pkg/cli/democluster/demo_cluster_test.go | 50 ++++++------ pkg/cli/flags.go | 5 +- pkg/server/BUILD.bazel | 1 + pkg/server/config.go | 6 ++ pkg/server/listen_and_update_addrs.go | 93 ++++++++++++++++++++++ pkg/server/server.go | 3 +- pkg/server/server_controller_new_server.go | 79 ++++++------------ pkg/server/start_listen.go | 5 +- pkg/server/tenant.go | 7 +- pkg/server/testserver.go | 7 +- pkg/util/netutil/addr/BUILD.bazel | 1 + pkg/util/netutil/addr/addr.go | 51 ++++++++++++ pkg/util/netutil/addr/addr_test.go | 34 ++++++++ 18 files changed, 309 insertions(+), 105 deletions(-) diff --git a/pkg/base/addr_validation.go b/pkg/base/addr_validation.go index bb94091fc3a4..66d1b47176f7 100644 --- a/pkg/base/addr_validation.go +++ b/pkg/base/addr_validation.go @@ -82,6 +82,12 @@ func (cfg *Config) ValidateAddrs(ctx context.Context) error { return invalidFlagErr(err, cliflags.ListenHTTPAddr) } cfg.HTTPAddr = net.JoinHostPort(httpHost, httpPort) + + // Validate secondary tenant port configuration. + if cfg.SecondaryTenantPortOffset > 0 && cfg.ApplicationInternalRPCPortMin > 0 { + return errors.Newf("cannot specify both %s and %s", cliflags.ApplicationInternalRPCPortRange.Name, cliflags.SecondaryTenantPortOffset.Name) + } + return nil } diff --git a/pkg/base/config.go b/pkg/base/config.go index ec89868a35f2..6c658d0ff50b 100644 --- a/pkg/base/config.go +++ b/pkg/base/config.go @@ -422,6 +422,13 @@ type Config struct { // See: https://github.com/cockroachdb/cockroach/issues/84585 SecondaryTenantPortOffset int + // ApplicationInternalRPCPortMin/PortMax define the range of TCP ports + // used to start the internal RPC service for application-level + // servers. This service is used for node-to-node RPC traffic and to + // serve data for 'debug zip'. + ApplicationInternalRPCPortMin int + ApplicationInternalRPCPortMax int + // Enables the use of an PTP hardware clock user space API for HLC current time. // This contains the path to the device to be used (i.e. /dev/ptp0) ClockDevicePath string @@ -495,6 +502,8 @@ func (cfg *Config) InitDefaults() { cfg.ClockDevicePath = "" cfg.AcceptSQLWithoutTLS = false cfg.SecondaryTenantPortOffset = 0 + cfg.ApplicationInternalRPCPortMin = 0 + cfg.ApplicationInternalRPCPortMax = 0 } // HTTPRequestScheme returns "http" or "https" based on the value of diff --git a/pkg/base/test_server_args.go b/pkg/base/test_server_args.go index 4c17579e5ff9..dda6123b2750 100644 --- a/pkg/base/test_server_args.go +++ b/pkg/base/test_server_args.go @@ -61,10 +61,12 @@ type TestServerArgs struct { // DisableTLSForHTTP if set, disables TLS for the HTTP interface. DisableTLSForHTTP bool - // SecondaryTenantPortOffset if non-zero forces the network addresses - // generated for servers started by the serverController to be offset - // from the base addressed by the specified amount. - SecondaryTenantPortOffset int + // ApplicationInternalRPCPortMin/PortMax define the range of TCP ports + // used to start the internal RPC service for application-level + // servers. This service is used for node-to-node RPC traffic and to + // serve data for 'debug zip'. + ApplicationInternalRPCPortMin int + ApplicationInternalRPCPortMax int // JoinAddr is the address of a node we are joining. // diff --git a/pkg/cli/cliflags/flags.go b/pkg/cli/cliflags/flags.go index 9653bed3a44e..9794bc9b9e22 100644 --- a/pkg/cli/cliflags/flags.go +++ b/pkg/cli/cliflags/flags.go @@ -149,6 +149,14 @@ accept requests.`, Description: "TCP port number offset to use for the secondary in-memory tenant.", } + ApplicationInternalRPCPortRange = FlagInfo{ + Name: "internal-rpc-port-range", + Description: ` +TCP port range to use for the internal RPC service for application-level servers. +This service is used for node-to-node RPC traffic and to serve data for 'debug zip'. +`, + } + SQLMem = FlagInfo{ Name: "max-sql-memory", Description: ` @@ -777,8 +785,8 @@ Disable use of "external" IO, such as to S3, GCS, or the file system (nodelocal) ExternalIOEnableNonAdminImplicitAndArbitraryOutbound = FlagInfo{ Name: "external-io-enable-non-admin-implicit-access", Description: ` -Allow non-admin users to specify arbitrary network addressses (e.g. https:// URIs or custom endpoints in s3:// URIs) and -implicit credentials (machine account/role providers) when running operations like IMPORT/EXPORT/BACKUP/etc. +Allow non-admin users to specify arbitrary network addressses (e.g. https:// URIs or custom endpoints in s3:// URIs) and +implicit credentials (machine account/role providers) when running operations like IMPORT/EXPORT/BACKUP/etc. Note: that --external-io-disable-http or --external-io-disable-implicit-credentials still apply, this only removes the admin-user requirement.`, } @@ -1647,7 +1655,7 @@ this flag is applied.`, ZipRedactLogs = FlagInfo{ Name: "redact-logs", Description: ` -DEPRECATED: Redact text that may contain confidential data or PII from +DEPRECATED: Redact text that may contain confidential data or PII from retrieved log entries.
 
@@ -1680,9 +1688,9 @@ For large clusters, this can dramatically increase debug zip size/file count.
 		Description: `
 Fetch stack traces for all goroutines running on each targeted node in nodes/*/stacks.txt
 and nodes/*/stacks_with_labels.txt files. Note that fetching stack traces for all goroutines is
-a "stop-the-world" operation, which can momentarily have negative impacts on SQL service 
-latency. Note that any periodic goroutine dumps previously taken on the node will still be 
-included in nodes/*/goroutines/*.txt.gz, as these would have already been generated and don't 
+a "stop-the-world" operation, which can momentarily have negative impacts on SQL service
+latency. Note that any periodic goroutine dumps previously taken on the node will still be
+included in nodes/*/goroutines/*.txt.gz, as these would have already been generated and don't
 require any additional stop-the-world operations to be collected.
 `,
 	}
@@ -1725,7 +1733,7 @@ dependencies on other tables.
 	ImportMaxRowSize = FlagInfo{
 		Name: "max-row-size",
 		Description: `
-Override limits on line size when importing Postgres dump files. This setting 
+Override limits on line size when importing Postgres dump files. This setting
 may need to be tweaked if the Postgres dump file has extremely long lines.
 `,
 	}
@@ -1846,7 +1854,7 @@ without any other details.
 	ExportDestination = FlagInfo{
 		Name: "destination",
 		Description: `
-The destination to export data. 
+The destination to export data.
 If the export format is readable and this flag left unspecified,
 defaults to display the exported data in the terminal output.
 `,
@@ -1855,7 +1863,7 @@ defaults to display the exported data in the terminal output.
 	ExportTableFormat = FlagInfo{
 		Name: "format",
 		Description: `
-Selects the format to export table rows from backups. 
+Selects the format to export table rows from backups.
 Only csv is supported at the moment.
 `,
 	}
@@ -1868,9 +1876,9 @@ Only csv is supported at the moment.
 	StartKey = FlagInfo{
 		Name: "start-key",
 		Description: `
-Start key and format as [:]. Supported formats: raw, hex, bytekey. 
+Start key and format as [:]. Supported formats: raw, hex, bytekey.
 The raw format supports escaped text. For example, "raw:\x01k" is
-the prefix for range local keys. 
+the prefix for range local keys.
 The bytekey format does not require table-key prefix.`,
 	}
 
@@ -1905,7 +1913,7 @@ host, or a full well-formed URI.
 
If a destination is not specified, the default URI scheme and host will be used, and the basename from the source will be used as the destination directory. -For example: 'userfile://defaultdb.public.userfiles_root/yourdirectory' +For example: 'userfile://defaultdb.public.userfiles_root/yourdirectory'
 
 
diff --git a/pkg/cli/democluster/demo_cluster.go b/pkg/cli/democluster/demo_cluster.go index e4bb37b8d504..6f88ffaacbbc 100644 --- a/pkg/cli/democluster/demo_cluster.go +++ b/pkg/cli/democluster/demo_cluster.go @@ -933,12 +933,15 @@ func (demoCtx *Context) testServerArgsForTransientCluster( args.Addr = fmt.Sprintf("127.0.0.1:%d", rpcPort) args.SQLAddr = fmt.Sprintf("127.0.0.1:%d", sqlPort) if !demoCtx.DisableServerController { - // The code in NewDemoCluster put the KV ports higher - // so we need to subtract the number of nodes to get - // back to the "good" ports. - // We reduce NumNodes by 1 because the server controller - // uses 1-based indexing for servers. - args.SecondaryTenantPortOffset = -(demoCtx.NumNodes + 1) + // The code in NewDemoCluster put the KV ports higher so + // we need to subtract the number of nodes to get back + // to the "good" ports. + // + // We reduce lower bound of the port range by 1 because + // the server controller uses 1-based indexing for + // servers. + args.ApplicationInternalRPCPortMin = rpcPort - (demoCtx.NumNodes + 1) + args.ApplicationInternalRPCPortMax = args.ApplicationInternalRPCPortMin + 1024 } } if httpPort := demoCtx.httpPort(serverIdx, forSystemTenant); httpPort != 0 { diff --git a/pkg/cli/democluster/demo_cluster_test.go b/pkg/cli/democluster/demo_cluster_test.go index bb5e4e0eb728..7a4a29a2de44 100644 --- a/pkg/cli/democluster/demo_cluster_test.go +++ b/pkg/cli/democluster/demo_cluster_test.go @@ -67,18 +67,19 @@ func TestTestServerArgsForTransientCluster(t *testing.T) { sqlPoolMemorySize: 2 << 10, cacheSize: 1 << 10, expected: base.TestServerArgs{ - DefaultTestTenant: base.TODOTestTenantDisabled, - PartOfCluster: true, - JoinAddr: "127.0.0.1", - DisableTLSForHTTP: true, - Addr: "127.0.0.1:1334", - SQLAddr: "127.0.0.1:1234", - HTTPAddr: "127.0.0.1:4567", - SecondaryTenantPortOffset: -2, - SQLMemoryPoolSize: 2 << 10, - CacheSize: 1 << 10, - NoAutoInitializeCluster: true, - EnableDemoLoginEndpoint: true, + DefaultTestTenant: base.TODOTestTenantDisabled, + PartOfCluster: true, + JoinAddr: "127.0.0.1", + DisableTLSForHTTP: true, + Addr: "127.0.0.1:1334", + SQLAddr: "127.0.0.1:1234", + HTTPAddr: "127.0.0.1:4567", + ApplicationInternalRPCPortMin: 1332, + ApplicationInternalRPCPortMax: 2356, + SQLMemoryPoolSize: 2 << 10, + CacheSize: 1 << 10, + NoAutoInitializeCluster: true, + EnableDemoLoginEndpoint: true, Knobs: base.TestingKnobs{ Server: &server.TestingKnobs{ StickyVFSRegistry: stickyVFSRegistry, @@ -92,18 +93,19 @@ func TestTestServerArgsForTransientCluster(t *testing.T) { sqlPoolMemorySize: 4 << 10, cacheSize: 4 << 10, expected: base.TestServerArgs{ - DefaultTestTenant: base.TODOTestTenantDisabled, - PartOfCluster: true, - JoinAddr: "127.0.0.1", - Addr: "127.0.0.1:1336", - SQLAddr: "127.0.0.1:1236", - HTTPAddr: "127.0.0.1:4569", - SecondaryTenantPortOffset: -2, - DisableTLSForHTTP: true, - SQLMemoryPoolSize: 4 << 10, - CacheSize: 4 << 10, - NoAutoInitializeCluster: true, - EnableDemoLoginEndpoint: true, + DefaultTestTenant: base.TODOTestTenantDisabled, + PartOfCluster: true, + JoinAddr: "127.0.0.1", + Addr: "127.0.0.1:1336", + SQLAddr: "127.0.0.1:1236", + HTTPAddr: "127.0.0.1:4569", + ApplicationInternalRPCPortMin: 1334, + ApplicationInternalRPCPortMax: 2358, + DisableTLSForHTTP: true, + SQLMemoryPoolSize: 4 << 10, + CacheSize: 4 << 10, + NoAutoInitializeCluster: true, + EnableDemoLoginEndpoint: true, Knobs: base.TestingKnobs{ Server: &server.TestingKnobs{ StickyVFSRegistry: stickyVFSRegistry, diff --git a/pkg/cli/flags.go b/pkg/cli/flags.go index ef5e53056180..eb5799eeedce 100644 --- a/pkg/cli/flags.go +++ b/pkg/cli/flags.go @@ -540,10 +540,13 @@ func init() { cliflagcfg.BoolFlag(f, &startBackground, cliflags.Background) } - // TODO(knz): Remove this port offset mechanism once we implement + // TODO(knz): Remove these port configuration mechanisms once we implement // a shared listener. See: https://github.com/cockroachdb/cockroach/issues/84585 cliflagcfg.IntFlag(f, &baseCfg.SecondaryTenantPortOffset, cliflags.SecondaryTenantPortOffset) _ = f.MarkHidden(cliflags.SecondaryTenantPortOffset.Name) + + cliflagcfg.VarFlag(f, addr.NewPortRangeSetter(&baseCfg.ApplicationInternalRPCPortMin, &baseCfg.ApplicationInternalRPCPortMax), cliflags.ApplicationInternalRPCPortRange) + _ = f.MarkHidden(cliflags.ApplicationInternalRPCPortRange.Name) } // Multi-tenancy start-sql command flags. diff --git a/pkg/server/BUILD.bazel b/pkg/server/BUILD.bazel index 83897cdf7cd6..04c2aae855a5 100644 --- a/pkg/server/BUILD.bazel +++ b/pkg/server/BUILD.bazel @@ -330,6 +330,7 @@ go_library( "//pkg/util/stop", "//pkg/util/strutil", "//pkg/util/syncutil", + "//pkg/util/sysutil", "//pkg/util/timeutil", "//pkg/util/timeutil/ptp", "//pkg/util/tracing", diff --git a/pkg/server/config.go b/pkg/server/config.go index dad8ede424cd..077bc3988f3e 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -267,6 +267,12 @@ type BaseConfig struct { // AutoConfigProvider provides auto-configuration tasks to apply on // the cluster during server initialization. AutoConfigProvider acprovider.Provider + + // RPCListenerFactory provides an alternate implementation of + // ListenAndUpdateAddrs for use when creating gPRC + // listeners. This is set by in-memory tenants if the user has + // specified port range preferences. + RPCListenerFactory RPCListenerFactory } // MakeBaseConfig returns a BaseConfig with default values. diff --git a/pkg/server/listen_and_update_addrs.go b/pkg/server/listen_and_update_addrs.go index 35dfcfe35f27..aa86ed5daa09 100644 --- a/pkg/server/listen_and_update_addrs.go +++ b/pkg/server/listen_and_update_addrs.go @@ -13,8 +13,11 @@ package server import ( "context" "net" + "strconv" "github.com/cockroachdb/cockroach/pkg/util/log" + "github.com/cockroachdb/cockroach/pkg/util/netutil/addr" + "github.com/cockroachdb/cockroach/pkg/util/sysutil" "github.com/cockroachdb/errors" ) @@ -32,6 +35,96 @@ func (l *ListenError) Error() string { return l.cause.Error() } // Unwrap is because ListenError is a wrapper. func (l *ListenError) Unwrap() error { return l.cause } +// secondaryTenantPortOffsetMaxPorts is used to calculate the maximum +// range of the ports allocated to RPC listeners when +// SecondaryTenantPortOffset is used. +const secondaryTenantPortOffsetMaxPorts = 1024 + +// ListenerFactoryForConfig return an RPCListenerFactory for the given +// configuration. If the configuration does not specify any secondary +// tenant port configuration, no factory is returned. +func ListenerFactoryForConfig(cfg *BaseConfig, portStartHint int) (RPCListenerFactory, error) { + if cfg.Config.SecondaryTenantPortOffset > 0 { + _, port, err := addr.SplitHostPort(cfg.Addr, "0") + if err != nil { + return nil, err + } + var pnum int + if port != "" { + pnum, err = strconv.Atoi(port) + if err != nil { + return nil, err + } + } + if pnum == 0 { + return nil, errors.Newf("no base port available for computation in %q", cfg.Addr) + } + + lowerBound := pnum + cfg.Config.SecondaryTenantPortOffset + rlf := &rangeListenerFactory{ + startHint: portStartHint, + lowerBound: lowerBound, + upperBound: lowerBound + secondaryTenantPortOffsetMaxPorts, + } + return rlf.ListenAndUpdateAddrs, nil + } + + if cfg.Config.ApplicationInternalRPCPortMin > 0 { + rlf := &rangeListenerFactory{ + startHint: portStartHint, + lowerBound: cfg.Config.ApplicationInternalRPCPortMin, + upperBound: cfg.Config.ApplicationInternalRPCPortMax, + } + return rlf.ListenAndUpdateAddrs, nil + } + + return nil, nil +} + +// The rangeListenerFactory tries to listen on a port between +// lowerBound and upperBound. The provided startHint allows the +// caller to specify an offset into the range to speed up port +// selection. +type rangeListenerFactory struct { + startHint int + lowerBound int + upperBound int +} + +func (rlf *rangeListenerFactory) ListenAndUpdateAddrs( + ctx context.Context, listenAddr, advertiseAddr *string, connName string, +) (net.Listener, error) { + h, _, err := addr.SplitHostPort(*listenAddr, "0") + if err != nil { + return nil, err + } + + if rlf.lowerBound > rlf.upperBound { + return nil, errors.AssertionFailedf("lower bound %d greater than upper bound %d", rlf.lowerBound, rlf.upperBound) + } + + numCandidates := (rlf.upperBound - rlf.lowerBound) + 1 + nextPort := rlf.lowerBound + (rlf.startHint % numCandidates) + + var ln net.Listener + for numAttempts := 0; numAttempts < numCandidates; numCandidates++ { + nextAddr := net.JoinHostPort(h, strconv.Itoa(nextPort)) + ln, err = net.Listen("tcp", nextAddr) + if err == nil { + if err := UpdateAddrs(ctx, listenAddr, advertiseAddr, ln.Addr()); err != nil { + return nil, errors.Wrapf(err, "internal error: cannot parse %s listen address", connName) + } + return ln, nil + } + if !sysutil.IsAddrInUse(err) { + return nil, err + } + + nextPort = ((nextPort - rlf.lowerBound + 1) % numCandidates) + rlf.lowerBound + } + return nil, errors.Wrapf(err, "port range (%d, %d) exhausted", rlf.lowerBound, rlf.upperBound) +} + // ListenAndUpdateAddrs starts a TCP listener on the specified address // then updates the address and advertised address fields based on the // actual interface address resolved by the OS during the Listen() diff --git a/pkg/server/server.go b/pkg/server/server.go index eeb42bec9cae..5751da2323bf 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1552,7 +1552,8 @@ func (s *topLevelServer) PreStart(ctx context.Context) error { // The SQL listener is returned, to start the SQL server later // below when the server has initialized. pgL, loopbackPgL, rpcLoopbackDialFn, startRPCServer, err := startListenRPCAndSQL( - ctx, workersCtx, s.cfg.BaseConfig, s.stopper, s.grpc, true /* enableSQLListener */) + ctx, workersCtx, s.cfg.BaseConfig, + s.stopper, s.grpc, ListenAndUpdateAddrs, true /* enableSQLListener */) if err != nil { return err } diff --git a/pkg/server/server_controller_new_server.go b/pkg/server/server_controller_new_server.go index f0c4e1405525..76ba3cc328dd 100644 --- a/pkg/server/server_controller_new_server.go +++ b/pkg/server/server_controller_new_server.go @@ -16,7 +16,6 @@ import ( "net/url" "os" "path/filepath" - "strconv" "github.com/cockroachdb/cockroach/pkg/base" "github.com/cockroachdb/cockroach/pkg/clusterversion" @@ -56,7 +55,7 @@ type tenantServerCreator interface { newTenantServer(ctx context.Context, tenantNameContainer *roachpb.TenantNameContainer, tenantStopper *stop.Stopper, - index int, + portStartHint int, testArgs base.TestSharedProcessTenantArgs, ) (onDemandServer, error) } @@ -68,14 +67,14 @@ func (s *topLevelServer) newTenantServer( ctx context.Context, tenantNameContainer *roachpb.TenantNameContainer, tenantStopper *stop.Stopper, - index int, + portStartHint int, testArgs base.TestSharedProcessTenantArgs, ) (onDemandServer, error) { tenantID, err := s.getTenantID(ctx, tenantNameContainer.Get()) if err != nil { return nil, err } - baseCfg, sqlCfg, err := s.makeSharedProcessTenantConfig(ctx, tenantID, index, tenantStopper) + baseCfg, sqlCfg, err := s.makeSharedProcessTenantConfig(ctx, tenantID, portStartHint, tenantStopper) if err != nil { return nil, err } @@ -150,7 +149,7 @@ func newTenantServerInternal( } func (s *topLevelServer) makeSharedProcessTenantConfig( - ctx context.Context, tenantID roachpb.TenantID, index int, stopper *stop.Stopper, + ctx context.Context, tenantID roachpb.TenantID, portStartHint int, stopper *stop.Stopper, ) (BaseConfig, SQLConfig, error) { // Create a configuration for the new tenant. parentCfg := s.cfg @@ -159,7 +158,7 @@ func (s *topLevelServer) makeSharedProcessTenantConfig( ServerInterceptors: s.grpc.serverInterceptorsInfo, SameProcessCapabilityAuthorizer: s.rpcContext.TenantRPCAuthorizer, } - baseCfg, sqlCfg, err := makeSharedProcessTenantServerConfig(ctx, tenantID, index, parentCfg, localServerInfo, stopper, s.recorder) + baseCfg, sqlCfg, err := makeSharedProcessTenantServerConfig(ctx, tenantID, portStartHint, parentCfg, localServerInfo, stopper, s.recorder) if err != nil { return BaseConfig{}, SQLConfig{}, err } @@ -171,7 +170,7 @@ func (s *topLevelServer) makeSharedProcessTenantConfig( func makeSharedProcessTenantServerConfig( ctx context.Context, tenantID roachpb.TenantID, - index int, + portStartHint int, kvServerCfg Config, kvServerInfo LocalKVServerInfo, stopper *stop.Stopper, @@ -248,13 +247,24 @@ func makeSharedProcessTenantServerConfig( baseCfg.HeapProfileDirName = kvServerCfg.BaseConfig.HeapProfileDirName baseCfg.GoroutineDumpDirName = kvServerCfg.BaseConfig.GoroutineDumpDirName + // The ListenerFactory allows us to dynamically choose a + // listen port based on the user's configuration. + // // TODO(knz): use a single network interface for all tenant servers. // See: https://github.com/cockroachdb/cockroach/issues/92524 - portOffset := kvServerCfg.Config.SecondaryTenantPortOffset - var err1, err2 error - baseCfg.Addr, err1 = rederivePort(index, kvServerCfg.Config.Addr, "", portOffset) - baseCfg.AdvertiseAddr, err2 = rederivePort(index, kvServerCfg.Config.AdvertiseAddr, baseCfg.Addr, portOffset) - if err := errors.CombineErrors(err1, err2); err != nil { + baseCfg.RPCListenerFactory, err = ListenerFactoryForConfig(&kvServerCfg.BaseConfig, portStartHint) + if err != nil { + return BaseConfig{}, SQLConfig{}, err + } + // We reset the port of the KV Addr configuration to 0. If we + // don't have a customized listener factory, this will force + // the operating system to choose a port by default. + baseCfg.Config.Addr, err = resetPort(kvServerCfg.Config.Addr) + if err != nil { + return BaseConfig{}, SQLConfig{}, err + } + baseCfg.Config.AdvertiseAddr, err = resetPort(kvServerCfg.Config.AdvertiseAddr) + if err != nil { return BaseConfig{}, SQLConfig{}, err } @@ -326,7 +336,6 @@ func makeSharedProcessTenantServerConfig( } sqlCfg = MakeSQLConfig(tenantID, tempStorageCfg) - baseCfg.Settings.ExternalIODir = kvServerCfg.BaseConfig.Settings.ExternalIODir sqlCfg.ExternalIODirConfig = kvServerCfg.SQLConfig.ExternalIODirConfig @@ -352,49 +361,13 @@ func makeSharedProcessTenantServerConfig( return baseCfg, sqlCfg, nil } -// rederivePort computes a host:port pair for a secondary tenant. -// TODO(knz): All this can be removed once we implement a single -// network listener. -// See https://github.com/cockroachdb/cockroach/issues/84604. -func rederivePort(index int, addrToChange string, prevAddr string, portOffset int) (string, error) { - h, port, err := addr.SplitHostPort(addrToChange, "0") +func resetPort(addrToChange string) (string, error) { + h, _, err := addr.SplitHostPort(addrToChange, "0") if err != nil { - return "", errors.Wrapf(err, "%d: %q", index, addrToChange) + return "", err } - if portOffset == 0 { - // Shortcut: random selection for base address. - return net.JoinHostPort(h, "0"), nil - } - - var pnum int - if port != "" { - pnum, err = strconv.Atoi(port) - if err != nil { - return "", errors.Wrapf(err, "%d: %q", index, addrToChange) - } - } - - if prevAddr != "" && pnum == 0 { - // Try harder to find a port number, by taking one from - // the previously computed addr. - _, port2, err := addr.SplitHostPort(prevAddr, "0") - if err != nil { - return "", errors.Wrapf(err, "%d: %q", index, prevAddr) - } - pnum, err = strconv.Atoi(port2) - if err != nil { - return "", errors.Wrapf(err, "%d: %q", index, prevAddr) - } - } - - // Do we have a base port to go with now? - if pnum == 0 { - // No, bail. - return "", errors.Newf("%d: no base port available for computation in %q / %q", index, addrToChange, prevAddr) - } - port = strconv.Itoa(pnum + portOffset + index) - return net.JoinHostPort(h, port), nil + return net.JoinHostPort(h, "0"), nil } func (s *SQLServerWrapper) reportTenantInfo(ctx context.Context) error { diff --git a/pkg/server/start_listen.go b/pkg/server/start_listen.go index e3155e582663..bb7dbd8da4f1 100644 --- a/pkg/server/start_listen.go +++ b/pkg/server/start_listen.go @@ -25,6 +25,8 @@ import ( "github.com/cockroachdb/errors" ) +type RPCListenerFactory func(ctx context.Context, addr, advertiseAddr *string, connName string) (net.Listener, error) + // startListenRPCAndSQL starts the RPC and SQL listeners. It returns: // - The listener for pgwire connections coming over the network. This will be used // to start the SQL server when initialization has completed. @@ -39,6 +41,7 @@ func startListenRPCAndSQL( cfg BaseConfig, stopper *stop.Stopper, grpc *grpcServer, + rpcListenerFactory RPCListenerFactory, enableSQLListener bool, ) ( sqlListener net.Listener, @@ -58,7 +61,7 @@ func startListenRPCAndSQL( } if ln == nil { var err error - ln, err = ListenAndUpdateAddrs(ctx, &cfg.Addr, &cfg.AdvertiseAddr, rpcChanName) + ln, err = rpcListenerFactory(ctx, &cfg.Addr, &cfg.AdvertiseAddr, rpcChanName) if err != nil { return nil, nil, nil, nil, err } diff --git a/pkg/server/tenant.go b/pkg/server/tenant.go index bc71abb5990e..dbfa4eac8eab 100644 --- a/pkg/server/tenant.go +++ b/pkg/server/tenant.go @@ -562,7 +562,12 @@ func (s *SQLServerWrapper) PreStart(ctx context.Context) error { // The SQL listener is returned, to start the SQL server later // below when the server has initialized. enableSQLListener := !s.sqlServer.cfg.DisableSQLListener - pgL, loopbackPgL, rpcLoopbackDialFn, startRPCServer, err := startListenRPCAndSQL(ctx, workersCtx, *s.sqlServer.cfg, s.stopper, s.grpc, enableSQLListener) + lf := ListenAndUpdateAddrs + if s.sqlServer.cfg.RPCListenerFactory != nil { + lf = s.sqlServer.cfg.RPCListenerFactory + } + + pgL, loopbackPgL, rpcLoopbackDialFn, startRPCServer, err := startListenRPCAndSQL(ctx, workersCtx, *s.sqlServer.cfg, s.stopper, s.grpc, lf, enableSQLListener) if err != nil { return err } diff --git a/pkg/server/testserver.go b/pkg/server/testserver.go index a4063ac009be..b269ddef7a13 100644 --- a/pkg/server/testserver.go +++ b/pkg/server/testserver.go @@ -236,8 +236,11 @@ func makeTestConfigFromParams(params base.TestServerArgs) Config { cfg.SQLAdvertiseAddr = util.IsolatedTestAddr.String() cfg.HTTPAddr = util.IsolatedTestAddr.String() } - if params.SecondaryTenantPortOffset != 0 { - cfg.SecondaryTenantPortOffset = params.SecondaryTenantPortOffset + if params.ApplicationInternalRPCPortMin != 0 { + cfg.ApplicationInternalRPCPortMin = params.ApplicationInternalRPCPortMin + } + if params.ApplicationInternalRPCPortMax != 0 { + cfg.ApplicationInternalRPCPortMax = params.ApplicationInternalRPCPortMax } if params.Addr != "" { cfg.Addr = params.Addr diff --git a/pkg/util/netutil/addr/BUILD.bazel b/pkg/util/netutil/addr/BUILD.bazel index c8898222037a..fee8e6a34602 100644 --- a/pkg/util/netutil/addr/BUILD.bazel +++ b/pkg/util/netutil/addr/BUILD.bazel @@ -18,5 +18,6 @@ go_test( deps = [ ":addr", "//pkg/testutils", + "@com_github_stretchr_testify//require", ], ) diff --git a/pkg/util/netutil/addr/addr.go b/pkg/util/netutil/addr/addr.go index 8899456e8ba5..ddf7abe19780 100644 --- a/pkg/util/netutil/addr/addr.go +++ b/pkg/util/netutil/addr/addr.go @@ -11,7 +11,9 @@ package addr import ( + "fmt" "net" + "strconv" "strings" "github.com/cockroachdb/errors" @@ -94,3 +96,52 @@ func (a addrSetter) Set(v string) error { *a.port = port return nil } + +type portRangeSetter struct { + lower *int + upper *int +} + +// NewPortRangeSetter creates a new pflag.Value that allows setting a +// lower and upper bound of a port range with a single setting. +func NewPortRangeSetter(lower, upper *int) pflag.Value { + return portRangeSetter{lower: lower, upper: upper} +} + +// String implements the pflag.Value interface. +func (a portRangeSetter) String() string { + return fmt.Sprintf("%d-%d", *a.lower, *a.upper) +} + +// Type implements the pflag.Value interface. +func (a portRangeSetter) Type() string { return "-" } + +// Set implements the pflag.Value interface. +func (a portRangeSetter) Set(v string) error { + parts := strings.Split(v, "-") + if len(parts) > 2 { + return errors.New("invalid port range: too many parts") + } + + if len(parts) < 2 || parts[1] == "" { + return errors.New("invalid port range: too few parts") + } + + lower, err := strconv.Atoi(parts[0]) + if err != nil { + return errors.Wrap(err, "invalid port range") + } + + upper, err := strconv.Atoi(parts[1]) + if err != nil { + return errors.Wrap(err, "invalid port range") + } + + if lower > upper { + return errors.Newf("invalid port range: lower bound (%d) > upper bound (%d)", lower, upper) + } + *a.lower = lower + *a.upper = upper + + return nil +} diff --git a/pkg/util/netutil/addr/addr_test.go b/pkg/util/netutil/addr/addr_test.go index 53975842c8af..17de7e2dd096 100644 --- a/pkg/util/netutil/addr/addr_test.go +++ b/pkg/util/netutil/addr/addr_test.go @@ -15,8 +15,42 @@ import ( "github.com/cockroachdb/cockroach/pkg/testutils" "github.com/cockroachdb/cockroach/pkg/util/netutil/addr" + "github.com/stretchr/testify/require" ) +func TestPortRangeSetter(t *testing.T) { + testData := []struct { + v string + lower int + upper int + str string + errStr string + }{ + {"5-10", 5, 10, "5-10", ""}, + {"5-", 5, 0, "", "too few parts"}, + {"5", 5, 0, "", "too few parts"}, + {"5-8-10", 0, 0, "", "too many parts"}, + {"a-5", 0, 0, "", "invalid syntax"}, + {"5-b", 0, 0, "", "invalid syntax"}, + {"10-5", 0, 0, "", "lower bound (10) > upper bound (5)"}, + } + for _, tc := range testData { + t.Run(tc.v, func(t *testing.T) { + var upper, lower int + s := addr.NewPortRangeSetter(&lower, &upper) + err := s.Set(tc.v) + if tc.errStr != "" { + require.ErrorContains(t, err, tc.errStr) + } else { + require.NoError(t, err) + require.Equal(t, tc.str, s.String()) + require.Equal(t, tc.upper, upper) + require.Equal(t, tc.lower, lower) + } + }) + } +} + func TestSplitHostPort(t *testing.T) { testData := []struct { v string From 9680b3eeea33879450ef1c40b04b30129ced3471 Mon Sep 17 00:00:00 2001 From: adityamaru Date: Mon, 9 Oct 2023 12:50:27 -0400 Subject: [PATCH 7/7] bulk: disable as-we-fill scatters for PCR In #111952 we saw log lines indicating that the SST batcher was attempting to scatter ranges that were bigger than our "max scatter size". This is likely because of the out-of-order nature of PCR's ingestion which means that the batcher is not guaranteed to have an empty RHS when it splits a soon-to-be overfull range. This change disable the as-we-fill scatters for PCR similar to how we disable them for restore. This is okay since the process already pre-splits and scatters the ranges into which the processors will ingest data. Informs: #111952 Release note: None --- pkg/kv/bulk/sst_batcher.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/kv/bulk/sst_batcher.go b/pkg/kv/bulk/sst_batcher.go index 62056e914c73..6ad67bbd7082 100644 --- a/pkg/kv/bulk/sst_batcher.go +++ b/pkg/kv/bulk/sst_batcher.go @@ -259,12 +259,19 @@ func MakeStreamSSTBatcher( ingestAll: true, mem: mem, limiter: sendLimiter, - // We use NormalPri since anything lower than normal - // priority is assumed to be able to handle reduced - // throughput. We are OK witht his for now since the - // consuming cluster of a replication stream does not - // have a latency sensitive workload running against - // it. + // disableScatters is set to true to disable scattering as-we-fill. The + // replication job already pre-splits and pre-scatters its target ranges to + // distribute the ingestion load. + // + // If the batcher knows that it is about to overfill a range, it always + // makes sense to split it before adding to it, rather than overfill it. It + // does not however make sense to scatter that range as the RHS maybe + // non-empty. + disableScatters: true, + // We use NormalPri since anything lower than normal priority is assumed to + // be able to handle reduced throughput. We are OK with his for now since + // the consuming cluster of a replication stream does not have a latency + // sensitive workload running against it. priority: admissionpb.NormalPri, } b.mu.lastFlush = timeutil.Now()