*** a/doc/src/sgml/func.sgml --- b/doc/src/sgml/func.sgml *************** *** 1519,1539 **** format format(formatstr text ! [, str "any" [, ...] ]) text Format arguments according to a format string. ! This function is similar to the C function ! sprintf, but only the following conversion specifications ! are recognized: %s interpolates the corresponding ! argument as a string; %I escapes its argument as ! an SQL identifier; %L escapes its argument as an ! SQL literal; %% outputs a literal %. ! A conversion can reference an explicit parameter position by preceding ! the conversion specifier with n$, where ! n is the argument position. ! See also . format('Hello %s, %1$s', 'World') Hello World, World --- 1519,1531 ---- format format(formatstr text ! [, formatarg "any" [, ...] ]) text Format arguments according to a format string. ! This function is similar to the C function sprintf. ! See . format('Hello %s, %1$s', 'World') Hello World, World *************** *** 2847,2852 **** --- 2839,3024 ---- + + <function>format</function> + + + format + + + + The function format produces formatted output according to + a format string in a similar way to the C function sprintf. + + + + + format(formatstr text [, formatarg "any" [, ...] ]) + + formatstr is a format string that specifies how the + result should be formatted. Text in the format string is copied directly + to the result, except where format specifiers are used. + Format specifiers act as placeholders in the string, allowing subsequent + function arguments to be formatted and inserted into the result. + + + + Format specifiers are introduced by a % character and take + the form + + %[parameter][flags][width]type + + + + parameter (optional) + + + An expression of the form n$ where + n is the index of the argument to use for the format + specifier's value. An index of 1 means the first argument after + formatstr. If the parameter field is + omitted, the default is to use the next argument. + + + SELECT format('Testing %s, %s, %s', 'one', 'two', 'three'); + Result: Testing one, two, three + + SELECT format('Testing %3$s, %2$s, %1$s', 'one', 'two', 'three'); + Result: Testing three, two, one + + + + Note that unlike the C function sprintf defined in the + Single UNIX Specification, the format function in + PostgreSQL allows format specifiers with and without + explicit parameter fields to be mixed in the same + format string. A format specifier without a + parameter field always uses the next argument after + the last argument consumed. In addition, the + PostgreSQL format function does not + require all function arguments to be referred to in the format + string. + + + SELECT format('Testing %3$s, %2$s, %s', 'one', 'two', 'three'); + Result: Testing three, two, three + + + + + + flags (optional) + + + Additional options controlling how the format specifier's output is + formatted. Currently the only supported flag is an minus sign + (-) which will cause the format specifier's output to be + left-aligned. This has no effect unless the width + field is also specified. + + + SELECT format('|%10s|%-10s|', 'foo', 'bar'); + Result: | foo|bar | + + + + + + width (optional) + + + Specifies the minimum number of characters to use to + display the format specifier's output. The width may be specified + using any of the following: a positive integer; an asterisk + (*) to use the next function argument as the width; or an + expression of the form *n$ to use the + nth function argument as the width. + + + + If the width comes from a function argument, that argument is + consumed before the argument that is used for the format + specifier's value. If the width argument is negative, the result is + left aligned, as if the - flag had been specified. + + + SELECT format('|%10s|', 'foo'); + Result: | foo| + + SELECT format('|%*s|', 10, 'foo'); + Result: | foo| + + SELECT format('|%*s|', -10, 'foo'); + Result: |foo | + + SELECT format('|%-*s|', 10, 'foo'); + Result: |foo | + + SELECT format('|%-*s|', -10, 'foo'); + Result: |foo | + + SELECT format('|%*2$s|', 'foo', 10, 'bar'); + Result: | bar| + + SELECT format('|%3$*2$s|', 'foo', 10, 'bar'); + Result: | bar| + + + + + + type (required) + + + The type of format conversion to use to produce the format + specifier's output. The following types are supported: + + + + s formats the argument value as a simple + string. A null value is treated as an empty string. + + + + + I escapes the value as an SQL identifier. It + is an error for the value to be null. + + + + + L escapes the value as an SQL literal. A null + value is displayed as the literal value NULL. + + + + + + SELECT format('Hello %s', 'World'); + Result: Hello World + + SELECT format('DROP TABLE %I', 'Foo bar'); + Result: DROP TABLE "Foo bar" + + SELECT format('SELECT %L', E'O\'Reilly'); + Result: SELECT 'O''Reilly' + + + + The %I and %L format specifiers may be used + to safely construct dynamic SQL statements. See + . + + + + + + + + In addition to the format specifiers above, the special escape sequence + %% may be used to output a literal % character. + + *** a/src/backend/utils/adt/varlena.c --- b/src/backend/utils/adt/varlena.c *************** *** 78,84 **** static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl); static StringInfo makeStringAggState(FunctionCallInfo fcinfo); static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, ! Datum value, bool isNull); static Datum text_to_array_internal(PG_FUNCTION_ARGS); static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, const char *fldsep, const char *null_string); --- 78,85 ---- static StringInfo makeStringAggState(FunctionCallInfo fcinfo); static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, ! Datum value, bool isNull, ! int flags, int width); static Datum text_to_array_internal(PG_FUNCTION_ARGS); static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, const char *fldsep, const char *null_string); *************** *** 3996,4001 **** text_reverse(PG_FUNCTION_ARGS) --- 3997,4133 ---- PG_RETURN_TEXT_P(result); } + #define FORWARD_PARSE_POINT(ptr) \ + do { \ + if (++(ptr) >= (end_ptr)) \ + ereport(ERROR, \ + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ + errmsg("unterminated conversion specifier"))); \ + } while (0) + + /* + * Parse congiguous digits into decimal number. + * + * Returns true if some digits could be parsed and *ptr moved to the next + * character to be parsed. The value is returned into *value. + */ + static bool + text_format_parse_digits(const char **ptr, const char *end_ptr, int *value) + { + const char *cp = *ptr; + int wval = 0; + bool found; + + /* + * continue, only when start_ptr is less than end_ptr. + * Overrun of cp is checked in FORWARD_PARSE_POINT. + */ + while (*cp >= '0' && *cp <= '9') + { + int newnum = wval * 10 + (*cp - '0'); + + if (newnum / 10 != wval) /* overflow? */ + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("number is out of range"))); + wval = newnum; + FORWARD_PARSE_POINT(cp); + } + + found = (cp > *ptr); + *value = wval; + *ptr = cp; + + return found; + } + + #define TEXT_FORMAT_FLAG_MINUS 0x0001 /* is minus in format string? */ + + #define SAMESIGN(a,b) (((a) < 0) == ((b) < 0)) + + /* + * parse format specification + * [argpos][flags][width]type + * + * Return values are, + * static const char * : Address to be parsed next. + * valarg : argument position for value to be printed. -1 means missing. + * widtharg : argument position for width. Zero means that argument position + * is not specified and -1 means missing. + * flags : flags + * width : the value for direct width specification, zero means that width + * is not specified. + */ + static const char * + text_format_parse_format(const char *start_ptr, const char *end_ptr, + int *valarg, int *widtharg, int *flags, int *width) + { + const char *cp = start_ptr; + int n; + + /* set defaults to out parameters */ + *valarg = -1; + *widtharg = -1; + *flags = 0; + *width = 0; + + /* try to identify first number */ + if (text_format_parse_digits(&cp, end_ptr, &n)) + { + if (*cp != '$') + { + *width = n; /* The number should be width */ + return cp; + } + /* Explicit 0 for argument index is immediately refused */ + if (n == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("conversion specifies argument 0, but arguments are numbered from 1"))); + *valarg = n; /* The number was argument position */ + FORWARD_PARSE_POINT(cp); + } + + /* Check for flags, only minus is supported now. */ + while (*cp == '-') + { + *flags = *flags | TEXT_FORMAT_FLAG_MINUS; + FORWARD_PARSE_POINT(cp); + } + + /* try to parse indirect width */ + if (*cp == '*') + { + FORWARD_PARSE_POINT(cp); + + if (text_format_parse_digits(&cp, end_ptr, &n)){ + /* number in this position should be closed by $ */ + if (*cp != '$') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unexpected char \"%c\".",*cp))); + FORWARD_PARSE_POINT(cp); + + /* Explicit 0 for argument index is immediately refused */ + if (n == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("conversion specifies argument 0, but arguments are numbered from 1"))); + *widtharg = n; + } + else + *widtharg = 0; /* 0 means argument position is not specified */ + + return cp; + } + + /* last possible number - width */ + if (text_format_parse_digits(&cp, end_ptr, &n)) + *width = n; + + return cp; + } + /* * Returns a formated string */ *************** *** 4016,4021 **** text_format(PG_FUNCTION_ARGS) --- 4148,4155 ---- Oid element_type = InvalidOid; Oid prev_type = InvalidOid; FmgrInfo typoutputfinfo; + FmgrInfo typoutputinfo_width; + Oid prev_type_width = InvalidOid; /* When format string is null, returns null */ if (PG_ARGISNULL(0)) *************** *** 4077,4083 **** text_format(PG_FUNCTION_ARGS) } /* Setup for main loop. */ ! fmt = PG_GETARG_TEXT_PP(0); start_ptr = VARDATA_ANY(fmt); end_ptr = start_ptr + VARSIZE_ANY_EXHDR(fmt); initStringInfo(&str); --- 4211,4217 ---- } /* Setup for main loop. */ ! fmt = PG_GETARG_TEXT_PP(arg++); start_ptr = VARDATA_ANY(fmt); end_ptr = start_ptr + VARSIZE_ANY_EXHDR(fmt); initStringInfo(&str); *************** *** 4085,4093 **** text_format(PG_FUNCTION_ARGS) /* Scan format string, looking for conversion specifiers. */ for (cp = start_ptr; cp < end_ptr; cp++) { ! Datum value; ! bool isNull; ! Oid typid; /* * If it's not the start of a conversion specifier, just copy it to --- 4219,4231 ---- /* Scan format string, looking for conversion specifiers. */ for (cp = start_ptr; cp < end_ptr; cp++) { ! Datum value; ! bool isNull; ! Oid typid; ! int valarg; ! int widtharg; ! int flags; ! int width; /* * If it's not the start of a conversion specifier, just copy it to *************** *** 4099,4109 **** text_format(PG_FUNCTION_ARGS) continue; } ! /* Did we run off the end of the string? */ ! if (++cp >= end_ptr) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ! errmsg("unterminated conversion specifier"))); /* Easy case: %% outputs a single % */ if (*cp == '%') --- 4237,4243 ---- continue; } ! FORWARD_PARSE_POINT(cp); /* Easy case: %% outputs a single % */ if (*cp == '%') *************** *** 4112,4184 **** text_format(PG_FUNCTION_ARGS) continue; } ! /* ! * If the user hasn't specified an argument position, we just advance ! * to the next one. If they have, we must parse it. ! */ ! if (*cp < '0' || *cp > '9') { ! ++arg; ! if (arg <= 0) /* overflow? */ ! { ! /* ! * Should not happen, as you can't pass billions of arguments ! * to a function, but better safe than sorry. ! */ ereport(ERROR, ! (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), ! errmsg("argument number is out of range"))); ! } ! } ! else ! { ! bool unterminated = false; ! /* Parse digit string. */ ! arg = 0; ! do { ! int newarg = arg * 10 + (*cp - '0'); ! if (newarg / 10 != arg) /* overflow? */ ! ereport(ERROR, ! (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), ! errmsg("argument number is out of range"))); ! arg = newarg; ! ++cp; ! } while (cp < end_ptr && *cp >= '0' && *cp <= '9'); /* ! * If we ran off the end, or if there's not a $ next, or if the $ ! * is the last character, the conversion specifier is improperly ! * terminated. */ ! if (cp == end_ptr || *cp != '$') ! unterminated = true; else { ! ++cp; ! if (cp == end_ptr) ! unterminated = true; } ! if (unterminated) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ! errmsg("unterminated conversion specifier"))); ! /* There's no argument 0. */ ! if (arg == 0) ereport(ERROR, ! (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ! errmsg("conversion specifies argument 0, but arguments are numbered from 1"))); } ! /* Not enough arguments? Deduct 1 to avoid counting format string. */ ! if (arg > nargs - 1) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("too few arguments for format"))); ! /* Get the value and type of the selected argument */ if (!funcvariadic) { --- 4246,4338 ---- continue; } ! cp = text_format_parse_format(cp, end_ptr, ! &valarg, &widtharg, &flags, &width); ! ! if (widtharg >= 0) { ! if (widtharg > 0) ! /* be consistent, move ordered argument together with ! * positional */ ! arg = widtharg; ! ! if (arg >= nargs) ereport(ERROR, ! (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ! errmsg("too few arguments for format"))); ! if (!funcvariadic) { ! value = PG_GETARG_DATUM(arg); ! isNull = PG_ARGISNULL(arg); ! typid = get_fn_expr_argtype(fcinfo->flinfo, arg); ! } ! else ! { ! value = elements[arg - 1]; ! isNull = nulls[arg - 1]; ! typid = element_type; ! } ! if (!OidIsValid(typid)) ! elog(ERROR, "could not determine data type of format() input"); ! arg++; /* ! * we don't need to different between NULL and zero in this moment, ! * NULL means ignore this width - same as zero. */ ! if (isNull) ! width = 0; ! else if (typid == INT4OID) ! width = DatumGetInt32(value); ! else if (typid == INT2OID) ! width = DatumGetInt16(value); else { ! char *str; ! ! /* simple IO cast to int */ ! if (typid != prev_type_width) ! { ! Oid typoutputfunc; ! bool typIsVarlena; ! ! getTypeOutputInfo(typid, &typoutputfunc, &typIsVarlena); ! fmgr_info(typoutputfunc, &typoutputinfo_width); ! prev_type_width = typid; ! } ! ! /* Stringify. */ ! str = OutputFunctionCall(&typoutputinfo_width, value); ! ! /* get int value */ ! width = pg_atoi(str, sizeof(int32), '\0'); ! pfree(str); } ! } ! /* Overflow check */ ! if (width != 0) ! { ! int32 _width = -width; ! ! if (SAMESIGN(width, _width)) ereport(ERROR, ! (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), ! errmsg("number is out of range"))); } ! if (valarg >= 0) ! /* be consistent, move ordered argument together with ! * positional */ ! arg = valarg; ! ! if (arg >= nargs) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("too few arguments for format"))); ! /* Get the value and type of the selected argument */ if (!funcvariadic) { *************** *** 4195,4200 **** text_format(PG_FUNCTION_ARGS) --- 4349,4356 ---- if (!OidIsValid(typid)) elog(ERROR, "could not determine data type of format() input"); + arg++; + /* * Get the appropriate typOutput function, reusing previous one if * same type as previous argument. That's particularly useful in the *************** *** 4221,4227 **** text_format(PG_FUNCTION_ARGS) case 'I': case 'L': text_format_string_conversion(&str, *cp, &typoutputfinfo, ! value, isNull); break; default: ereport(ERROR, --- 4377,4384 ---- case 'I': case 'L': text_format_string_conversion(&str, *cp, &typoutputfinfo, ! value, isNull, ! flags, width); break; default: ereport(ERROR, *************** *** 4244,4288 **** text_format(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } /* Format a %s, %I, or %L conversion. */ static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, ! Datum value, bool isNull) { char *str; - /* Handle NULL arguments before trying to stringify the value. */ if (isNull) { ! if (conversion == 'L') ! appendStringInfoString(buf, "NULL"); else if (conversion == 'I') ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("null values cannot be formatted as an SQL identifier"))); return; } - /* Stringify. */ str = OutputFunctionCall(typOutputInfo, value); /* Escape. */ if (conversion == 'I') { /* quote_identifier may or may not allocate a new string. */ ! appendStringInfoString(buf, quote_identifier(str)); } else if (conversion == 'L') ! { ! char *qstr = quote_literal_cstr(str); ! appendStringInfoString(buf, qstr); /* quote_literal_cstr() always allocates a new string */ pfree(qstr); } else ! appendStringInfoString(buf, str); /* Cleanup. */ pfree(str); --- 4401,4490 ---- PG_RETURN_TEXT_P(result); } + /* + * Add spaces on begin or on end when it is necessary + */ + static void + text_format_append_string(StringInfo buf, const char *str, + int flags, int width) + { + bool align_to_left = false; + int len; + + /* fast path */ + if (width == 0) + { + appendStringInfoString(buf, str); + return; + } + else if (width < 0 || (flags & TEXT_FORMAT_FLAG_MINUS)) + { + align_to_left = true; + if (width < 0) + width = -width; + } + + len = pg_mbstrlen(str); + if (align_to_left) + { + appendStringInfoString(buf, str); + if (len < width) + appendStringInfoSpaces(buf, width - len); + } + else + { + /* align_to_right */ + if (len < width) + appendStringInfoSpaces(buf, width - len); + appendStringInfoString(buf, str); + } + } + /* Format a %s, %I, or %L conversion. */ static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, ! Datum value, bool isNull, ! int flags, int width) { char *str; if (isNull) { ! if (conversion == 's') ! text_format_append_string(buf, "", ! flags, width); ! else if (conversion == 'L') ! text_format_append_string(buf, "NULL", ! flags, width); else if (conversion == 'I') ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("null values cannot be formatted as an SQL identifier"))); + return; } str = OutputFunctionCall(typOutputInfo, value); /* Escape. */ if (conversion == 'I') { /* quote_identifier may or may not allocate a new string. */ ! text_format_append_string(buf, quote_identifier(str), ! flags, width); } else if (conversion == 'L') ! { char *qstr = quote_literal_cstr(str); ! text_format_append_string(buf, qstr, ! flags, width); /* quote_literal_cstr() always allocates a new string */ pfree(qstr); } else ! text_format_append_string(buf, str, ! flags, width); /* Cleanup. */ pfree(str); *** a/src/test/regress/expected/text.out --- b/src/test/regress/expected/text.out *************** *** 256,267 **** select format('%1$s %4$s', 1, 2, 3); ERROR: too few arguments for format select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); ERROR: too few arguments for format select format('%1s', 1); ! ERROR: unterminated conversion specifier select format('%1$', 1); ERROR: unterminated conversion specifier select format('%1$1', 1); ! ERROR: unrecognized conversion specifier "1" -- check mix of positional and ordered placeholders select format('Hello %s %1$s %s', 'World', 'Hello again'); format --- 256,275 ---- ERROR: too few arguments for format select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); ERROR: too few arguments for format + select format('%0$s', 'Hello'); + ERROR: conversion specifies argument 0, but arguments are numbered from 1 + select format('%*0$s', 'Hello'); + ERROR: conversion specifies argument 0, but arguments are numbered from 1 select format('%1s', 1); ! format ! -------- ! 1 ! (1 row) ! select format('%1$', 1); ERROR: unterminated conversion specifier select format('%1$1', 1); ! ERROR: unterminated conversion specifier -- check mix of positional and ordered placeholders select format('Hello %s %1$s %s', 'World', 'Hello again'); format *************** *** 328,330 **** from generate_series(1,200) g(i); --- 336,409 ---- 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200 (1 row) + -- left, right align + select format('>>%10s<<', 'Hello') + union all + select format('>>%10s<<', NULL) + union all + select format('>>%10s<<', '') + union all + select format('>>%-10s<<', '') + union all + select format('>>%-10s<<', 'Hello') + union all + select format('>>%-10s<<', NULL) + union all + select format('>>%1$10s<<', 'Hello') + union all + select format('>>%1$-10I<<', 'Hello') + union all + select format('>>%2$*1$L<<', 10, 'Hello') + union all + select format('>>%2$*1$L<<', 10, NULL) + union all + select format('>>%2$*1$L<<', -10, NULL) + union all + select format('>>%*s<<', 10, 'Hello'); + format + ---------------- + >> Hello<< + >> << + >> << + >> << + >>Hello << + >> << + >> Hello<< + >>"Hello" << + >> 'Hello'<< + >> NULL<< + >>NULL << + >> Hello<< + (12 rows) + + select format('>>%*1$s<<', 10, 'Hello'); + format + ---------------- + >> Hello<< + (1 row) + + select format('>>%-s<<', 'Hello'); + format + ----------- + >>Hello<< + (1 row) + + -- NULL is not different to zero here + select format('>>%10L<<', NULL); + format + ---------------- + >> NULL<< + (1 row) + + select format('>>%2$*1$L<<', NULL, 'Hello'); + format + ------------- + >>'Hello'<< + (1 row) + + select format('>>%2$*1$L<<', 0, 'Hello'); + format + ------------- + >>'Hello'<< + (1 row) + *** a/src/test/regress/sql/text.sql --- b/src/test/regress/sql/text.sql *************** *** 78,83 **** select format('%1$s %12$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); --- 78,85 ---- -- should fail select format('%1$s %4$s', 1, 2, 3); select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + select format('%0$s', 'Hello'); + select format('%*0$s', 'Hello'); select format('%1s', 1); select format('%1$', 1); select format('%1$1', 1); *************** *** 97,99 **** select format('Hello', variadic NULL); --- 99,134 ---- -- variadic argument allows simulating more than FUNC_MAX_ARGS parameters select format(string_agg('%s',','), variadic array_agg(i)) from generate_series(1,200) g(i); + + -- left, right align + select format('>>%10s<<', 'Hello') + union all + select format('>>%10s<<', NULL) + union all + select format('>>%10s<<', '') + union all + select format('>>%-10s<<', '') + union all + select format('>>%-10s<<', 'Hello') + union all + select format('>>%-10s<<', NULL) + union all + select format('>>%1$10s<<', 'Hello') + union all + select format('>>%1$-10I<<', 'Hello') + union all + select format('>>%2$*1$L<<', 10, 'Hello') + union all + select format('>>%2$*1$L<<', 10, NULL) + union all + select format('>>%2$*1$L<<', -10, NULL) + union all + select format('>>%*s<<', 10, 'Hello'); + + select format('>>%*1$s<<', 10, 'Hello'); + select format('>>%-s<<', 'Hello'); + + -- NULL is not different to zero here + select format('>>%10L<<', NULL); + select format('>>%2$*1$L<<', NULL, 'Hello'); + select format('>>%2$*1$L<<', 0, 'Hello');