Skip to content

Commit 977fa2b

Browse files
committed
sql, opt/exec: add UpdateSwap and DeleteSwap to exec factory
Add constructors for UpdateSwap and DeleteSwap to the exec factory. These closely resemble the constructors for Update and Delete, but have one additional check: 1. That all columns in the primary index are included in fetch columns. Informs: #144503 Release note: None
1 parent ac076a5 commit 977fa2b

File tree

6 files changed

+289
-1
lines changed

6 files changed

+289
-1
lines changed

pkg/sql/delete_swap.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,7 @@ func (d *deleteSwapNode) Close(ctx context.Context) {
142142
func (d *deleteSwapNode) rowsWritten() int64 {
143143
return d.run.td.rowsWritten
144144
}
145+
146+
func (d *deleteSwapNode) enableAutoCommit() {
147+
d.run.td.enableAutoCommit()
148+
}

pkg/sql/distsql_spec_exec_factory.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,21 @@ func (e *distSQLSpecExecFactory) ConstructUpdate(
14181418
return nil, unimplemented.NewWithIssue(47473, "experimental opt-driven distsql planning: update")
14191419
}
14201420

1421+
func (e *distSQLSpecExecFactory) ConstructUpdateSwap(
1422+
input exec.Node,
1423+
table cat.Table,
1424+
fetchCols exec.TableColumnOrdinalSet,
1425+
updateCols exec.TableColumnOrdinalSet,
1426+
returnCols exec.TableColumnOrdinalSet,
1427+
passthrough colinfo.ResultColumns,
1428+
lockedIndexes cat.IndexOrdinals,
1429+
autoCommit bool,
1430+
) (exec.Node, error) {
1431+
return nil, unimplemented.NewWithIssue(
1432+
47473, "experimental opt-driven distsql planning: update swap",
1433+
)
1434+
}
1435+
14211436
func (e *distSQLSpecExecFactory) ConstructUpsert(
14221437
input exec.Node,
14231438
table cat.Table,
@@ -1448,6 +1463,20 @@ func (e *distSQLSpecExecFactory) ConstructDelete(
14481463
return nil, unimplemented.NewWithIssue(47473, "experimental opt-driven distsql planning: delete")
14491464
}
14501465

1466+
func (e *distSQLSpecExecFactory) ConstructDeleteSwap(
1467+
input exec.Node,
1468+
table cat.Table,
1469+
fetchCols exec.TableColumnOrdinalSet,
1470+
returnCols exec.TableColumnOrdinalSet,
1471+
passthrough colinfo.ResultColumns,
1472+
lockedIndexes cat.IndexOrdinals,
1473+
autoCommit bool,
1474+
) (exec.Node, error) {
1475+
return nil, unimplemented.NewWithIssue(
1476+
47473, "experimental opt-driven distsql planning: delete swap",
1477+
)
1478+
}
1479+
14511480
func (e *distSQLSpecExecFactory) ConstructDeleteRange(
14521481
table cat.Table,
14531482
needed exec.TableColumnOrdinalSet,

pkg/sql/opt/exec/explain/plan_gist_factory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import (
3535
)
3636

3737
func init() {
38-
if numOperators != 65 {
38+
if numOperators != 67 {
3939
// This error occurs when an operator has been added or removed in
4040
// pkg/sql/opt/exec/explain/factory.opt. If an operator is added at the
4141
// end of factory.opt, simply adjust the hardcoded value above. If an

pkg/sql/opt/exec/factory.opt

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,9 @@ define Delete {
652652
# - the input to the delete is a scan (without limits);
653653
# - there are no inbound FKs to the table.
654654
#
655+
# If both DeleteSwap and DeleteRange could be used for a DELETE statement,
656+
# DeleteSwap takes precedence.
657+
#
655658
# See the comment for ConstructScan for descriptions of the needed and
656659
# indexConstraint parameters, since DeleteRange combines Delete + Scan into a
657660
# single operator.
@@ -888,3 +891,111 @@ define VectorMutationSearch {
888891
# the search must return the quantized vector.
889892
IsIndexPut bool
890893
}
894+
895+
# UpdateSwap performs an UPDATE statement as a compare-and-swap operation, which
896+
# only succeeds if the existing row matches what was expected. This
897+
# compare-and-swap allows the statement to skip the initial fetch from the
898+
# target table, which changes some simple UPDATE statements from 2 round trips
899+
# to 1 round trip.
900+
#
901+
# This fast path is only possible when certain conditions hold true:
902+
# - all columns in the primary index are constrained to a single exact value
903+
# by the WHERE clause;
904+
# - only a single row is modified;
905+
# - there are no FK checks or cascades;
906+
# - there are no uniqueness checks;
907+
# - there are no check constraints;
908+
# - there are no after triggers.
909+
#
910+
# Multiple indexes and complex projections are supported.
911+
#
912+
# For example, the following statement would be able to use UpdateSwap, as it
913+
# only updates a single row and all columns are constrained to a single exact
914+
# value by the WHERE clause.
915+
#
916+
# CREATE TABLE abcd (
917+
# a INT PRIMARY KEY,
918+
# b INT,
919+
# c INT,
920+
# d INT AS (b + c) VIRTUAL,
921+
# INDEX (b)
922+
# );
923+
#
924+
# UPDATE abcd SET a = b + 1, c = 3
925+
# WHERE a = 1
926+
# AND b IS NOT DISTINCT FROM 2
927+
# AND c IS NOT DISTINCT FROM NULL
928+
# RETURNING c, d;
929+
#
930+
# The parameters are mostly the same as for Update.
931+
define UpdateSwap {
932+
Input exec.Node
933+
Table cat.Table
934+
FetchCols exec.TableColumnOrdinalSet
935+
UpdateCols exec.TableColumnOrdinalSet
936+
ReturnCols exec.TableColumnOrdinalSet
937+
Passthrough colinfo.ResultColumns
938+
939+
# If set, the input has already acquired the locks during the initial scan
940+
# of the Update (i.e. on the "old" KVs within these indexes).
941+
LockedIndexes cat.IndexOrdinals
942+
943+
# If set, the operator will commit the transaction as part of its execution.
944+
AutoCommit bool
945+
}
946+
947+
# DeleteSwap performs a DELETE statement as a compare-and-swap operation, which
948+
# only succeeds if the existing row matches what was expected. This
949+
# compare-and-swap allows the statement to skip the initial fetch from the
950+
# target table, which changes some simple DELETE statements from 2 round trips
951+
# to 1 round trip.
952+
#
953+
# This fast path is only possible when certain conditions hold true:
954+
# - all columns in the primary index are constrained to a single exact value
955+
# by the WHERE clause;
956+
# - only a single row is deleted;
957+
# - there are no FK checks or cascades;
958+
# - there are no uniqueness checks;
959+
# - there are no after triggers.
960+
#
961+
# Multiple indexes and complex projections are supported.
962+
#
963+
# For example, the following statement would be able to use DeleteSwap, as it
964+
# only deletes a single row and all columns are constrained to a single exact
965+
# value by the WHERE clause.
966+
#
967+
# CREATE TABLE abcd (
968+
# a INT PRIMARY KEY,
969+
# b INT,
970+
# c INT,
971+
# d INT AS (b + c) VIRTUAL,
972+
# INDEX (b)
973+
# );
974+
#
975+
# DELETE FROM abcd
976+
# WHERE a = 1
977+
# AND b IS NOT DISTINCT FROM 2
978+
# AND c IS NOT DISTINCT FROM NULL
979+
# RETURNING *;
980+
#
981+
# If both DeleteSwap and DeleteRange could be used for a DELETE statement,
982+
# DeleteSwap takes precedence.
983+
#
984+
# The parameters are the same as for Delete.
985+
define DeleteSwap {
986+
Input exec.Node
987+
Table cat.Table
988+
FetchCols exec.TableColumnOrdinalSet
989+
ReturnCols exec.TableColumnOrdinalSet
990+
Passthrough colinfo.ResultColumns
991+
992+
# If set, the input has already acquired the locks on the specified indexes
993+
# on all keys that might be deleted from that index.
994+
LockedIndexes cat.IndexOrdinals
995+
996+
# If set, the operator will commit the transaction as part of its execution.
997+
# This is false when executing inside an explicit transaction, or there are
998+
# multiple mutations in a statement, or the output of the mutation is
999+
# processed through side-effecting expressions.
1000+
AutoCommit bool
1001+
}

pkg/sql/opt_exec_factory.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,81 @@ func (ef *execFactory) ConstructUpdate(
16301630
return &rowCountNode{source: upd}, nil
16311631
}
16321632

1633+
func (ef *execFactory) ConstructUpdateSwap(
1634+
input exec.Node,
1635+
table cat.Table,
1636+
fetchColOrdSet exec.TableColumnOrdinalSet,
1637+
updateColOrdSet exec.TableColumnOrdinalSet,
1638+
returnColOrdSet exec.TableColumnOrdinalSet,
1639+
passthrough colinfo.ResultColumns,
1640+
lockedIndexes cat.IndexOrdinals,
1641+
autoCommit bool,
1642+
) (exec.Node, error) {
1643+
// TODO(radu): the execution code has an annoying limitation that the fetch
1644+
// columns must be a superset of the update columns, even when the "old" value
1645+
// of a column is not necessary. The optimizer code for pruning columns is
1646+
// aware of this limitation.
1647+
if !updateColOrdSet.SubsetOf(fetchColOrdSet) {
1648+
return nil, errors.AssertionFailedf("execution requires all update columns have a fetch column")
1649+
}
1650+
1651+
// For update swap, fetch columns need to include at least every column that
1652+
// could appear in the primary index.
1653+
primaryIndex := table.Index(cat.PrimaryIndex)
1654+
for i := 0; i < primaryIndex.ColumnCount(); i++ {
1655+
col := primaryIndex.Column(i)
1656+
if col.Kind() == cat.System {
1657+
continue
1658+
}
1659+
if !fetchColOrdSet.Contains(col.Ordinal()) {
1660+
return nil, errors.AssertionFailedf("fetch columns missing col %v", col.ColName())
1661+
}
1662+
}
1663+
1664+
rowsNeeded := !returnColOrdSet.Empty()
1665+
tabDesc := table.(*optTable).desc
1666+
1667+
upd := updateSwapNodePool.Get().(*updateSwapNode)
1668+
*upd = updateSwapNode{
1669+
singleInputPlanNode: singleInputPlanNode{input.(planNode)},
1670+
}
1671+
1672+
// If rows are not needed, no columns are returned.
1673+
var returnCols []catalog.Column
1674+
if rowsNeeded {
1675+
returnCols = makeColList(table, returnColOrdSet)
1676+
upd.columns = colinfo.ResultColumnsFromColumns(tabDesc.GetID(), returnCols)
1677+
// Add the passthrough columns to the returning columns.
1678+
upd.columns = append(upd.columns, passthrough...)
1679+
}
1680+
1681+
// Create the table updater, which does the bulk of the work.
1682+
if err := ef.constructUpdateRun(
1683+
&upd.run, table, fetchColOrdSet, updateColOrdSet, returnColOrdSet, rowsNeeded,
1684+
returnCols, exec.CheckOrdinalSet{} /* checks */, passthrough,
1685+
nil /* uniqueWithTombstoneIndexes */, lockedIndexes,
1686+
); err != nil {
1687+
return nil, err
1688+
}
1689+
1690+
if autoCommit {
1691+
upd.enableAutoCommit()
1692+
}
1693+
1694+
// Serialize the data-modifying plan to ensure that no data is observed that
1695+
// hasn't been validated first. See the comments on BatchedNext() in
1696+
// plan_batch.go.
1697+
if rowsNeeded {
1698+
return &spoolNode{
1699+
singleInputPlanNode: singleInputPlanNode{&serializeNode{source: upd}},
1700+
}, nil
1701+
}
1702+
1703+
// We could use serializeNode here, but using rowCountNode is an
1704+
// optimization that saves on calls to Next() by the caller.
1705+
return &rowCountNode{source: upd}, nil
1706+
}
1707+
16331708
func (ef *execFactory) constructUpdateRun(
16341709
run *updateRun,
16351710
table cat.Table,
@@ -1839,6 +1914,71 @@ func (ef *execFactory) ConstructDelete(
18391914
return &rowCountNode{source: del}, nil
18401915
}
18411916

1917+
func (ef *execFactory) ConstructDeleteSwap(
1918+
input exec.Node,
1919+
table cat.Table,
1920+
fetchColOrdSet exec.TableColumnOrdinalSet,
1921+
returnColOrdSet exec.TableColumnOrdinalSet,
1922+
passthrough colinfo.ResultColumns,
1923+
lockedIndexes cat.IndexOrdinals,
1924+
autoCommit bool,
1925+
) (exec.Node, error) {
1926+
// For delete swap, fetch columns need to include at least every column that
1927+
// could appear in the primary index.
1928+
primaryIndex := table.Index(cat.PrimaryIndex)
1929+
for i := 0; i < primaryIndex.ColumnCount(); i++ {
1930+
col := primaryIndex.Column(i)
1931+
if col.Kind() == cat.System {
1932+
continue
1933+
}
1934+
if !fetchColOrdSet.Contains(col.Ordinal()) {
1935+
return nil, errors.AssertionFailedf("fetch columns missing col %v", col.ColName())
1936+
}
1937+
}
1938+
1939+
rowsNeeded := !returnColOrdSet.Empty()
1940+
tabDesc := table.(*optTable).desc
1941+
1942+
// Now make a delete node. We use a pool.
1943+
del := deleteSwapNodePool.Get().(*deleteSwapNode)
1944+
*del = deleteSwapNode{
1945+
singleInputPlanNode: singleInputPlanNode{input.(planNode)},
1946+
}
1947+
1948+
// If rows are not needed, no columns are returned.
1949+
var returnCols []catalog.Column
1950+
if rowsNeeded {
1951+
returnCols = makeColList(table, returnColOrdSet)
1952+
// Delete returns the non-mutation columns specified, in the same
1953+
// order they are defined in the table.
1954+
del.columns = colinfo.ResultColumnsFromColumns(tabDesc.GetID(), returnCols)
1955+
// Add the passthrough columns to the returning columns.
1956+
del.columns = append(del.columns, passthrough...)
1957+
}
1958+
1959+
// Create the table deleter, which does the bulk of the work.
1960+
ef.constructDeleteRun(
1961+
&del.run, table, fetchColOrdSet, rowsNeeded, returnCols, passthrough, lockedIndexes,
1962+
)
1963+
1964+
if autoCommit {
1965+
del.enableAutoCommit()
1966+
}
1967+
1968+
// Serialize the data-modifying plan to ensure that no data is observed that
1969+
// hasn't been validated first. See the comments on BatchedNext() in
1970+
// plan_batch.go.
1971+
if rowsNeeded {
1972+
return &spoolNode{
1973+
singleInputPlanNode: singleInputPlanNode{&serializeNode{source: del}},
1974+
}, nil
1975+
}
1976+
1977+
// We could use serializeNode here, but using rowCountNode is an
1978+
// optimization that saves on calls to Next() by the caller.
1979+
return &rowCountNode{source: del}, nil
1980+
}
1981+
18421982
func (ef *execFactory) constructDeleteRun(
18431983
run *deleteRun,
18441984
table cat.Table,

pkg/sql/update_swap.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,7 @@ func (u *updateSwapNode) Close(ctx context.Context) {
142142
func (u *updateSwapNode) rowsWritten() int64 {
143143
return u.run.tu.rowsWritten
144144
}
145+
146+
func (u *updateSwapNode) enableAutoCommit() {
147+
u.run.tu.enableAutoCommit()
148+
}

0 commit comments

Comments
 (0)