diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 1e7345014295..9200b240dcf5 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -3161,6 +3161,9 @@ Case mode values range between 0 - 1, representing lower casing and upper casing Immutable substring(input: varbit, start_pos: int, length: int) → varbit

Returns a bit subarray of input starting at start_pos (count starts at 1) and including up to length characters.

Immutable +substring_index(input: string, delim: string, count: int) → string

Returns a substring of input before count occurrences of delim. +If count is positive, the leftmost part is returned. If count is negative, the rightmost part is returned.

+
Immutable to_char_with_style(date: date, datestyle: string) → string

Convert an date to a string assuming the string is formatted using the given DateStyle.

Immutable to_char_with_style(interval: interval, style: string) → string

Convert an interval to a string using the given IntervalStyle.

diff --git a/pkg/sql/logictest/testdata/logic_test/builtin_function b/pkg/sql/logictest/testdata/logic_test/builtin_function index eb4309f037df..11c1d204a52c 100644 --- a/pkg/sql/logictest/testdata/logic_test/builtin_function +++ b/pkg/sql/logictest/testdata/logic_test/builtin_function @@ -4407,4 +4407,106 @@ SELECT crdb_internal.type_is_indexable(NULL); ---- NULL +subtest substring_index + +# Test basic behavior of substring_index +query T +SELECT substring_index('www.cockroachlabs.com', '.', 2); +---- +www.cockroachlabs + +query T +SELECT substring_index('www.cockroachlabs.com', '.', -2); +---- +cockroachlabs.com + +query T +SELECT substring_index('hello.world.example.com', '.', 3); +---- +hello.world.example + +query T +SELECT substring_index('hello.world.example.com', '.', -1); +---- +com + +# Test when count is 0, should return empty string +query T +SELECT substring_index('111-22222-3333', '-', 0); +---- +· + +# Test when count exceeds available delimiters, should return full string +query T +SELECT substring_index('example.com', '.', 5); +---- +example.com + +query T +SELECT substring_index('example.com', '.', -5); +---- +example.com + +# Test when delimiter is not found in the string, should return full string +query T +SELECT substring_index('no.delimiters.here', ':', 1); +---- +no.delimiters.here + +query T +SELECT substring_index('singleword', '.', 1); +---- +singleword + +# Test when input is empty, should return empty string +query T +SELECT substring_index('', '.', 1); +---- +· + + +# Test when delimiter is empty, should return empty string +query T +SELECT substring_index('teststring', '', 1); +---- +· + +# Test NULL behavior, should return NULL if any argument is NULL +query T +SELECT substring_index(NULL, '.', 1); +---- +NULL + +query T +SELECT substring_index('test.string', NULL, 1); +---- +NULL + +query T +SELECT substring_index('test.string', '.', NULL); +---- +NULL + +# Test with multi-character delimiters +query T +SELECT substring_index('apple--banana--cherry--date', '--', 2); +---- +apple--banana + +query T +SELECT substring_index('apple--banana--cherry--date', '--', -2); +---- +cherry--date + +# Test when the string contains repeated delimiters +query T +SELECT substring_index('a..b..c..d', '..', 2); +---- +a..b + +query T +SELECT substring_index('a..b..c..d', '..', -2); +---- +c..d + subtest end diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go index 952f22fa8002..bf2ecc266b6b 100644 --- a/pkg/sql/sem/builtins/builtins.go +++ b/pkg/sql/sem/builtins/builtins.go @@ -387,6 +387,50 @@ var regularBuiltins = map[string]builtinDefinition{ "substr": makeSubStringImpls(), "substring": makeSubStringImpls(), + "substring_index": makeBuiltin( + tree.FunctionProperties{Category: builtinconstants.CategoryString}, + tree.Overload{ + Types: tree.ParamTypes{ + {Name: "input", Typ: types.String}, + {Name: "delim", Typ: types.String}, + {Name: "count", Typ: types.Int}, + }, + ReturnType: tree.FixedReturnType(types.String), + Fn: func(_ context.Context, _ *eval.Context, args tree.Datums) (tree.Datum, error) { + input := string(tree.MustBeDString(args[0])) + delim := string(tree.MustBeDString(args[1])) + count := int(tree.MustBeDInt(args[2])) + + // Handle empty input. + if input == "" || delim == "" || count == 0 { + return tree.NewDString(""), nil + } + + parts := strings.Split(input, delim) + length := len(parts) + + // If count is positive, return the first 'count' parts joined by delim + if count > 0 { + if count >= length { + return tree.NewDString(input), nil // If count exceeds occurrences, return the full string + } + result := strings.Join(parts[:count], delim) + return tree.NewDString(result), nil + } + + // If count is negative, return the last 'abs(count)' parts joined by delim + count = -count + if count >= length { + return tree.NewDString(input), nil // If count exceeds occurrences, return the full string + } + return tree.NewDString(strings.Join(parts[length-count:], delim)), nil + }, + Info: "Returns a substring of `input` before `count` occurrences of `delim`.\n" + + "If `count` is positive, the leftmost part is returned. If `count` is negative, the rightmost part is returned.", + Volatility: volatility.Immutable, + }, + ), + // concat concatenates the text representations of all the arguments. // NULL arguments are ignored. "concat": makeBuiltin( diff --git a/pkg/sql/sem/builtins/fixed_oids.go b/pkg/sql/sem/builtins/fixed_oids.go index d40303780631..413f2e373ec5 100644 --- a/pkg/sql/sem/builtins/fixed_oids.go +++ b/pkg/sql/sem/builtins/fixed_oids.go @@ -2643,6 +2643,7 @@ var builtinOidsArray = []string{ 2680: `jsonpath(jsonpath: jsonpath) -> jsonpath`, 2681: `varchar(jsonpath: jsonpath) -> varchar`, 2682: `char(jsonpath: jsonpath) -> "char"`, + 2683: `substring_index(input: string, delim: string, count: int) -> string`, } var builtinOidsBySignature map[string]oid.Oid