Skip to content

Commit 5c3e649

Browse files
authored
Merge pull request #156779 from mgartner/backport25.4-156573
release-25.4: opt: fix LTREE <@ index constraint spans
2 parents 004caca + 6c32d90 commit 5c3e649

File tree

6 files changed

+57
-59
lines changed

6 files changed

+57
-59
lines changed

pkg/sql/logictest/testdata/logic_test/ltree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,21 @@ SELECT lca(''::LTREE, 'A.B.C'::LTREE)
350350
----
351351
NULL
352352

353+
# Regression test for #156478. Correct constraint spans should be built for the
354+
# <@ (contained-by) operator.
355+
statement ok
356+
CREATE TABLE t156478 (
357+
k INT PRIMARY KEY,
358+
l LTREE,
359+
INDEX idx (l),
360+
INDEX idx_desc (l DESC)
361+
)
362+
363+
statement ok
364+
INSERT INTO t156478 VALUES (1, 'foo.bar_'::LTREE)
365+
366+
query empty
367+
SELECT * FROM t156478@idx WHERE l <@ 'foo.bar'::LTREE
353368

369+
query empty
370+
SELECT * FROM t156478@idx_desc WHERE l <@ 'foo.bar'::LTREE

pkg/sql/opt/exec/execbuilder/testdata/select_index

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1943,4 +1943,4 @@ vectorized: true
19431943
• scan
19441944
missing stats
19451945
table: t5@t5_a_idx
1946-
spans: [/'A.B.C' - /'A.B.D')
1946+
spans: [/'A.B.C' - /'A.B.C-')

pkg/sql/opt/idxconstraint/index_constraints.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,15 @@ func (c *indexConstraintCtx) makeSpansForSingleColumnDatum(
375375

376376
case opt.ContainedByOp:
377377
if l, ok := datum.(*tree.DLTree); ok {
378-
if l.LTree.Compare(ltree.Empty) == 0 {
379-
// Empty LTree shouldn't be constrained.
380-
// TODO: This could be constrained by excluding NULLs.
378+
end, ok := l.LTree.NextSibling()
379+
if !ok {
380+
// An empty LTree represents the root of the tree, so it
381+
// includes all non-NULL LTrees.
382+
// TODO(mgartner): This could be constrained by excluding NULLs.
381383
break
382384
}
383385
startKey := constraint.MakeKey(l)
384-
endKey := constraint.MakeKey(tree.NewDLTree(l.LTree.NextSibling()))
386+
endKey := constraint.MakeKey(tree.NewDLTree(end))
385387
c.singleSpan(
386388
offset, startKey, includeBoundary, endKey, excludeBoundary,
387389
c.columns[offset].Descending(),

pkg/sql/opt/idxconstraint/testdata/ltree

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ a @> NULL
2525
index-constraints vars=(a ltree) index=a
2626
a <@ 'A.B.C'
2727
----
28-
[/'A.B.C' - /'A.B.D')
28+
[/'A.B.C' - /'A.B.C-')
2929

3030
index-constraints vars=(a ltree) index=a
3131
a <@ 'z'
@@ -53,23 +53,23 @@ a @> '-' OR a @> 'foo.bar'
5353
index-constraints vars=(a ltree) index=a
5454
a <@ '-' OR a <@ 'foo.bar'
5555
----
56-
[/'-' - /'0')
57-
[/'foo.bar' - /'foo.bas')
56+
[/'-' - /'--')
57+
[/'foo.bar' - /'foo.bar-')
5858

5959
index-constraints vars=(a ltree) index=a
6060
a <@ 'A.B.C' OR a @> 'A.B'
6161
----
6262
[/'' - /'']
6363
[/'A' - /'A']
6464
[/'A.B' - /'A.B']
65-
[/'A.B.C' - /'A.B.D')
65+
[/'A.B.C' - /'A.B.C-')
6666

6767
index-constraints vars=(a ltree) index=a
6868
a @> 'A.B.C' OR a <@ 'A.B'
6969
----
7070
[/'' - /'']
7171
[/'A' - /'A']
72-
[/'A.B' - /'A.C')
72+
[/'A.B' - /'A.B-')
7373

7474
index-constraints vars=(a ltree) index=a
7575
a <@ 'A.B.C' AND a @> 'A.B.C.D.E'

pkg/util/ltree/ltree.go

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
const (
1919
PathSeparator = "."
20+
minCharacter = "-"
2021
// Postgres imposes a 65535 limit on the number of labels in a ltree.
2122
maxNumOfLabels = 65535
2223
// Postgres docs mention labels must be less than 256 bytes, but in practice,
@@ -297,44 +298,17 @@ func LCA(ltrees []T) (_ T, isNull bool) { // lint: uppercase function OK
297298
}
298299

299300
// NextSibling returns a LTree with a lexicographically incremented last label.
300-
// This is different from the next lexicographic LTree. This is mainly used for
301-
// defining a key-encoded span for ancestry operators.
302-
// Note that this could produce a LTREE with more labels than the maximum
303-
// allowed.
304-
// Example: 'A.B' -> 'A.C'
305-
func (lt T) NextSibling() T {
301+
// If the LTree is empty, it returns ok=false. An empty LTree represents the
302+
// root of the tree, so it has no siblings.
303+
//
304+
// This is mainly used for defining a key-encoded span for ancestry operators.
305+
//
306+
// Example: 'A.B' -> 'A.B-'
307+
func (lt T) NextSibling() (_ T, ok bool) {
306308
if lt.Len() == 0 {
307-
return Empty
309+
return T{}, false
308310
}
309-
lastLabel := lt.path[len(lt.path)-1]
310-
nextLabel := incrementLabel(lastLabel)
311-
312-
newLTree := lt.Copy()
313-
newLTree.path[newLTree.Len()-1] = nextLabel
314-
return newLTree
315-
}
316-
317-
var nextCharMap = map[byte]byte{
318-
'-': '0',
319-
'9': 'A',
320-
'Z': '_',
321-
'_': 'a',
322-
'z': 0,
323-
}
324-
325-
func incrementLabel(label string) string {
326-
nextLabel := []byte(label)
327-
nextChar, ok := nextCharMap[nextLabel[len(nextLabel)-1]]
328-
329-
if ok && nextChar == 0 {
330-
// Technically, this could mean exceeding the length of the label.
331-
return string(append(nextLabel, '-'))
332-
}
333-
334-
if !ok {
335-
nextChar = nextLabel[len(nextLabel)-1] + 1
336-
}
337-
338-
nextLabel[len(nextLabel)-1] = nextChar
339-
return string(nextLabel)
311+
newLT := lt.Copy()
312+
newLT.path[newLT.Len()-1] = newLT.path[newLT.Len()-1] + minCharacter
313+
return newLT, true
340314
}

pkg/util/ltree/ltree_test.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,33 @@ func TestCompare(t *testing.T) {
153153
}
154154
}
155155

156-
func TestNext(t *testing.T) {
156+
func TestNextSibling(t *testing.T) {
157157
tests := []struct {
158158
input string
159159
expected string
160+
ok bool
160161
}{
161-
{"a", "b"},
162-
{"a.b", "a.c"},
163-
{"a.z", "a.z-"},
164-
{"-", "0"},
165-
{"9", "A"},
166-
{"Z", "_"},
167-
{"_", "a"},
162+
{"", "", false},
163+
{"a", "a-", true},
164+
{"a.b", "a.b-", true},
165+
{"a.z", "a.z-", true},
166+
{"-", "--", true},
167+
{"9", "9-", true},
168+
{"Z", "Z-", true},
169+
{"_", "_-", true},
168170
}
169171

170172
for _, tc := range tests {
171173
a, err := ParseLTree(tc.input)
172174
if err != nil {
173175
t.Fatalf("unexpected error parsing %q: %v", tc.input, err)
174176
}
175-
next := a.NextSibling()
176-
if next.String() != tc.expected {
177-
t.Errorf("expected next of %q to be %q, got %q", tc.input, tc.expected, next.String())
177+
next, ok := a.NextSibling()
178+
if ok != tc.ok {
179+
t.Errorf("expected ok=%v for input %q, got %v", tc.ok, tc.input, ok)
180+
}
181+
if n := next.String(); n != tc.expected {
182+
t.Errorf("expected %q for input %q, got %q", tc.expected, tc.input, n)
178183
}
179184
}
180185
}

0 commit comments

Comments
 (0)