Skip to content

Commit

Permalink
sql, builtins: implement format
Browse files Browse the repository at this point in the history
Closes cockroachdb#68330.

Unfortunately hooking into fmt by implementing the Formatter interface
doesn't give us the postgres-style syntax, so instead I've
implemented my own very stripped-down version of fmt by
copying golang's.

Release note (sql change): Added the format builtin function.
format interpolates arguments into a string in the style of C's sprintf.
For example, format('Hello, %s', 'world') returns 'Hello, world'.
  • Loading branch information
HonoreDB committed Jul 16, 2022
1 parent 0e816ef commit 493d596
Show file tree
Hide file tree
Showing 12 changed files with 850 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2698,6 +2698,8 @@ The output can be used to recreate a database.’</p>
</span></td><td>Immutable</td></tr>
<tr><td><a name="encode"></a><code>encode(data: <a href="bytes.html">bytes</a>, format: <a href="string.html">string</a>) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Encodes <code>data</code> using <code>format</code> (<code>hex</code> / <code>escape</code> / <code>base64</code>).</p>
</span></td><td>Immutable</td></tr>
<tr><td><a name="format"></a><code>format(<a href="string.html">string</a>, anyelement...) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Interprets the first argument as a format string similar to C sprintf and interpolates the remaining arguments.</p>
</span></td><td>Stable</td></tr>
<tr><td><a name="from_ip"></a><code>from_ip(val: <a href="bytes.html">bytes</a>) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Converts the byte string representation of an IP to its character string representation.</p>
</span></td><td>Immutable</td></tr>
<tr><td><a name="from_uuid"></a><code>from_uuid(val: <a href="bytes.html">bytes</a>) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Converts the byte string representation of a UUID to its character string representation.</p>
Expand Down
4 changes: 4 additions & 0 deletions pkg/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ ALL_TESTS = [
"//pkg/sql/schemachanger/screl:screl_test",
"//pkg/sql/schemachanger/scrun:scrun_test",
"//pkg/sql/schemachanger:schemachanger_test",
"//pkg/sql/sem/builtins/pgformat:pgformat_test",
"//pkg/sql/sem/builtins:builtins_disallowed_imports_test",
"//pkg/sql/sem/builtins:builtins_test",
"//pkg/sql/sem/cast:cast_test",
Expand Down Expand Up @@ -1531,6 +1532,8 @@ GO_TARGETS = [
"//pkg/sql/sem/asof:asof",
"//pkg/sql/sem/builtins/builtinconstants:builtinconstants",
"//pkg/sql/sem/builtins/builtinsregistry:builtinsregistry",
"//pkg/sql/sem/builtins/pgformat:pgformat",
"//pkg/sql/sem/builtins/pgformat:pgformat_test",
"//pkg/sql/sem/builtins:builtins",
"//pkg/sql/sem/builtins:builtins_test",
"//pkg/sql/sem/cast:cast",
Expand Down Expand Up @@ -2506,6 +2509,7 @@ GET_X_DATA_TARGETS = [
"//pkg/sql/sem/builtins:get_x_data",
"//pkg/sql/sem/builtins/builtinconstants:get_x_data",
"//pkg/sql/sem/builtins/builtinsregistry:get_x_data",
"//pkg/sql/sem/builtins/pgformat:get_x_data",
"//pkg/sql/sem/cast:get_x_data",
"//pkg/sql/sem/catconstants:get_x_data",
"//pkg/sql/sem/catid:get_x_data",
Expand Down
55 changes: 55 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/builtin_function
Original file line number Diff line number Diff line change
Expand Up @@ -2482,6 +2482,61 @@ SELECT array_to_string(NULL, ','), array_to_string(NULL, 'foo', 'zerp')
----
NULL NULL

# Examples from https://www.postgresql.org/docs/9.3/functions-string.html#FUNCTIONS-STRING-FORMAT
query T
SELECT format('Hello %s', 'World')
----
Hello World

query T
SELECT format('INSERT INTO %I VALUES(%L)', 'locations', 'C:\Program Files')
----
INSERT INTO locations VALUES(e'C:\\Program Files')

query T
SELECT format('|%10s|', 'foo')
----
| foo|

query T
SELECT format('|%-10s|', 'foo')
----
|foo |

query T
SELECT format('|%*s|', 10, 'foo')
----
| foo|

query T
SELECT format('|%*s|', -10, 'foo')
----
|foo |

query T
SELECT format('|%-*s|', 10, 'foo')
----
|foo |

query T
SELECT format('|%-*s|', -10, 'foo')
----
|foo |

# Escaping $ into \x24 only needed in testlogic or prepared statements
query T
SELECT format(E'Testing %3\x24s, %2\x24s, %1\x24s', 'one', 'two', 'three')
----
Testing three, two, one

query T
SELECT format(E'Testing %3\x24s, %2\x24s, %s', 'one', 'two', 'three')
----
Testing three, two, three

query error pq: format\(\): error parsing format string: not enough arguments
SELECT format(E'%2\x24s','foo');

subtest pg_is_in_recovery

query B colnames
Expand Down
199 changes: 199 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/format
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# LogicTest: !fakedist-spec-planning

# tests from https://github.com/postgres/postgres/blob/4ca9985957881c223b4802d309c0bbbcf8acd1c1/src/test/regress/sql/text.sql#L55

query T
select format(NULL)
----
NULL

query T
select format('Hello')
----
Hello

query T
select format('Hello %s', 'World')
----
Hello World

query T
select format('Hello %%')
----
Hello %

query T
select format('Hello %%%%')
----
Hello %%

query error pq: format\(\): error parsing format string: not enough arguments
select format('Hello %s %s', 'World')

query error pq: format\(\): error parsing format string: not enough arguments
select format('Hello %s')

query error pq: format\(\): error parsing format string: unrecognized verb x
select format('Hello %x', 20)

query T
select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, 'Hello')
----
INSERT INTO mytab VALUES('10','Hello')

query T
select format('%s%s%s','Hello', NULL,'World')
----
HelloWorld

query T
select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, NULL)
----
INSERT INTO mytab VALUES('10',NULL)

query T
select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', NULL, 'Hello');
----
INSERT INTO mytab VALUES(NULL,'Hello')

query error pq: format\(\): error parsing format string: NULL cannot be formatted as a SQL identifier
select format('INSERT INTO %I VALUES(%L,%L)', NULL, 10, 'Hello')

# Many of the below tests involve strings with a literal $.
# This can break TestLogic under some conditions. If you're seeing mysterious errors in this file,
# they can likely be fixed by escaping $ into \x24, e.g. replace '%1$s' with E'%\x24s'.
# For now, strings are left unescaped here for readability.
query T
select format('%1$s %3$s', 1, 2, 3)
----
1 3

query T
select format('%1$s %12$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
----
1 12

query error pq: format\(\): error parsing format string: not enough arguments
select format('%1$s %4$s', 1, 2, 3)

query error pq: format\(\): error parsing format string: not enough arguments
select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)

query error pq: format\(\): error parsing format string: positions must be positive and 1-indexed
select format('%0$s', 'Hello')

query error pq: format\(\): error parsing format string: positions must be positive and 1-indexed
select format('%*0$s', 'Hello')

query error pq: format\(\): error parsing format string: unterminated format specifier
select format('%1$', 1)

query error pq: format\(\): error parsing format string: unterminated format specifier
select format('%1$1', 1)

query error pq: format\(\): error parsing format string: unterminated format specifier
select format('%1$1', 1)

# Mixing positional and non-positional placeholders is allowed here, unusually.
# A non-positional placeholder consumes the argument after the last one,
# whether or not the last one was positional.

query T
select format('Hello %s %1$s %s', 'World', 'Hello again')
----
Hello World World Hello again

query T
select format('Hello %s %s, %2$s %2$s', 'World', 'Hello again')
----
Hello World Hello again, Hello again Hello again

query T
select format('>>%10s<<', 'Hello')
----
>> Hello<<

query T
select format('>>%10s<<', NULL)
----
>> <<

query T
select format('>>%10s<<', '')
----
>> <<

query T
select format('>>%-10s<<', '')
----
>> <<

query T
select format('>>%-10s<<', 'Hello')
----
>>Hello <<

query T
select format('>>%-10s<<', NULL)
----
>> <<

query T
select format('>>%1$10s<<', 'Hello')
----
>> Hello<<

query T
select format('>>%1$-10I<<', 'Hello')
----
>>"Hello" <<

query T
select format('>>%2$*1$L<<', 10, 'Hello')
----
>> 'Hello'<<

query T
select format('>>%2$*1$L<<', 10, NULL)
----
>> NULL<<

query T
select format('>>%*s<<', 10, 'Hello')
----
>> Hello<<

query T
select format('>>%*1$s<<', 10, 'Hello')
----
>> Hello<<

query T
select format('>>%-s<<', 'Hello')
----
>>Hello<<

query T
select format('>>%10L<<', NULL)
----
>> NULL<<

# Null is equivalent to zero minimum width.
# Zero minimum width has no effect.
query T
select format('>>%2$*1$L<<', NULL, 'Hello')
----
>>'Hello'<<

query T
select format('>>%2$*1$L<<', 0, 'Hello')
----
>>'Hello'<<

# This is an error in postgres, but our
# implementation allows width and position flags
# to be in either order.
query T
select format('>>%*1$2$L<<', 10, 'Hello')
----
>> 'Hello'<<
2 changes: 2 additions & 0 deletions pkg/sql/sem/builtins/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ go_library(
"//pkg/sql/sem/asof",
"//pkg/sql/sem/builtins/builtinconstants",
"//pkg/sql/sem/builtins/builtinsregistry",
"//pkg/sql/sem/builtins/pgformat",
"//pkg/sql/sem/catconstants",
"//pkg/sql/sem/catid",
"//pkg/sql/sem/eval",
Expand Down Expand Up @@ -157,6 +158,7 @@ go_test(
"//pkg/sql/randgen",
"//pkg/sql/sem/builtins/builtinconstants",
"//pkg/sql/sem/builtins/builtinsregistry",
"//pkg/sql/sem/builtins/pgformat",
"//pkg/sql/sem/eval",
"//pkg/sql/sem/tree",
"//pkg/sql/sem/tree/treewindow",
Expand Down
24 changes: 24 additions & 0 deletions pkg/sql/sem/builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/sql/rowenc/keyside"
"github.com/cockroachdb/cockroach/pkg/sql/sem/asof"
"github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/builtinconstants"
"github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/pgformat"
"github.com/cockroachdb/cockroach/pkg/sql/sem/catid"
"github.com/cockroachdb/cockroach/pkg/sql/sem/eval"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
Expand Down Expand Up @@ -223,6 +224,8 @@ var regularBuiltins = map[string]builtinDefinition{
),
),

"format": formatImpls,

"octet_length": makeBuiltin(tree.FunctionProperties{Category: builtinconstants.CategoryString},
stringOverload1(
func(_ *eval.Context, s string) (tree.Datum, error) {
Expand Down Expand Up @@ -7197,6 +7200,27 @@ var lengthImpls = func(incBitOverload bool) builtinDefinition {
return b
}

var formatImpls = makeBuiltin(tree.FunctionProperties{Category: builtinconstants.CategoryString},
tree.Overload{
Types: tree.VariadicType{FixedTypes: []*types.T{types.String}, VarType: types.Any},
ReturnType: tree.FixedReturnType(types.String),
Fn: func(ctx *eval.Context, args tree.Datums) (tree.Datum, error) {
if args[0] == tree.DNull {
return tree.DNull, nil
}
formatStr := tree.MustBeDString(args[0])
formatArgs := args[1:]
str, err := pgformat.Format(ctx, string(formatStr), formatArgs...)
if err != nil {
return nil, pgerror.Wrap(err, pgcode.InvalidParameterValue, "error parsing format string")
}
return tree.NewDString(str), nil
},
Info: "Interprets the first argument as a format string similar to C sprintf and interpolates the remaining arguments.",
Volatility: volatility.Stable,
NullableArgs: true,
})

var substringImpls = makeBuiltin(tree.FunctionProperties{Category: builtinconstants.CategoryString},
tree.Overload{
Types: tree.ArgTypes{
Expand Down
Loading

0 comments on commit 493d596

Please sign in to comment.