From 73e7025bd8eed941a068f0a7a71e02dca8d38d1c Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 14 Mar 2013 22:56:56 -0400 Subject: [PATCH] Extend format() to handle field width and left/right alignment. This change adds some more standard sprintf() functionality to format(). Pavel Stehule, reviewed by Dean Rasheed and Kyotaro Horiguchi --- doc/src/sgml/func.sgml | 222 +++++++++++++++- src/backend/utils/adt/varlena.c | 395 +++++++++++++++++++++++------ src/test/regress/expected/text.out | 115 ++++++++- src/test/regress/sql/text.sql | 21 +- 4 files changed, 656 insertions(+), 97 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 372e2b6575..896c08c09c 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -1519,21 +1519,13 @@ format format(formatstr text - [, str "any" [, ...] ]) + [, formatarg "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 . + This function is similar to the C function sprintf. + See . format('Hello %s, %1$s', 'World') Hello World, World @@ -2847,6 +2839,214 @@ + + <function>format</function> + + + format + + + + The function format produces output formatted according to + a format string, in a style similar 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, defining how + subsequent function arguments should be formatted and inserted into the + result. Each formatarg argument is converted to text + according to the usual output rules for its data type, and then formatted + and inserted into the result string according to the format specifier(s). + + + + Format specifiers are introduced by a % character and have + the form + +%[position][flags][width]type + + where the component fields are: + + + + position (optional) + + + A string of the form n$ where + n is the index of the argument to print. + Index 1 means the first argument after + formatstr. If the position is + omitted, the default is to use the next argument in sequence. + + + + + + flags (optional) + + + Additional options controlling how the format specifier's output is + formatted. Currently the only supported flag is a minus sign + (-) which will cause the format specifier's output to be + left-justified. This has no effect unless the width + field is also specified. + + + + + + width (optional) + + + Specifies the minimum number of characters to use to + display the format specifier's output. The output is padded on the + left or right (depending on the - flag) with spaces as + needed to fill the width. A too-small width does not cause + truncation of the output, but is simply ignored. 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 a string 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) within a + field of length abs(width). + + + + + + 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 treats the argument value as an SQL + identifier, double-quoting it if necessary. + It is an error for the value to be null. + + + + + L quotes the argument value as an SQL literal. + A null value is displayed as the string NULL, without + quotes. + + + + + + + + + + + In addition to the format specifiers described above, the special sequence + %% may be used to output a literal % character. + + + + Here are some examples of the basic format conversions: + + +SELECT format('Hello %s', 'World'); +Result: Hello World + +SELECT format('Testing %s, %s, %s, %%', 'one', 'two', 'three'); +Result: Testing one, two, three, % + +SELECT format('INSERT INTO %I VALUES(%L)', 'Foo bar', E'O\'Reilly'); +Result: INSERT INTO "Foo bar" VALUES('O''Reilly') + +SELECT format('INSERT INTO %I VALUES(%L)', 'locations', E'C:\\Program Files'); +Result: INSERT INTO locations VALUES(E'C:\\Program Files') + + + + + Here are examples using width fields + and the - flag: + + +SELECT format('|%10s|', 'foo'); +Result: | foo| + +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 | + + + + + These examples show use of position fields: + + +SELECT format('Testing %3$s, %2$s, %1$s', 'one', 'two', 'three'); +Result: Testing three, two, one + +SELECT format('|%*2$s|', 'foo', 10, 'bar'); +Result: | bar| + +SELECT format('|%1$*2$s|', 'foo', 10, 'bar'); +Result: | foo| + + + + + Unlike the standard C function sprintf, + PostgreSQL's format function allows format + specifiers with and without position fields to be mixed + in the same format string. A format specifier without a + position field always uses the next argument after the + last argument consumed. + In addition, the format function does not require all + function arguments to be used in the format string. + For example: + + +SELECT format('Testing %3$s, %2$s, %s', 'one', 'two', 'three'); +Result: Testing three, two, three + + + + + The %I and %L format specifiers are particularly + useful for safely constructing dynamic SQL statements. See + . + + + diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index e69b7dd3e6..f41abe3b2e 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -56,32 +56,41 @@ typedef struct #define PG_GETARG_UNKNOWN_P_COPY(n) DatumGetUnknownPCopy(PG_GETARG_DATUM(n)) #define PG_RETURN_UNKNOWN_P(x) PG_RETURN_POINTER(x) -static int text_cmp(text *arg1, text *arg2, Oid collid); static int32 text_length(Datum str); -static int text_position(text *t1, text *t2); -static void text_position_setup(text *t1, text *t2, TextPositionState *state); -static int text_position_next(int start_pos, TextPositionState *state); -static void text_position_cleanup(TextPositionState *state); static text *text_catenate(text *t1, text *t2); static text *text_substring(Datum str, int32 start, int32 length, bool length_not_specified); static text *text_overlay(text *t1, text *t2, int sp, int sl); -static void appendStringInfoText(StringInfo str, const text *t); +static int text_position(text *t1, text *t2); +static void text_position_setup(text *t1, text *t2, TextPositionState *state); +static int text_position_next(int start_pos, TextPositionState *state); +static void text_position_cleanup(TextPositionState *state); +static int text_cmp(text *arg1, text *arg2, Oid collid); static bytea *bytea_catenate(bytea *t1, bytea *t2); static bytea *bytea_substring(Datum str, int S, int L, bool length_not_specified); 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 void appendStringInfoText(StringInfo str, const text *t); 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); +static StringInfo makeStringAggState(FunctionCallInfo fcinfo); +static bool text_format_parse_digits(const char **ptr, const char *end_ptr, + int *value); +static const char *text_format_parse_format(const char *start_ptr, + const char *end_ptr, + int *argpos, int *widthpos, + int *flags, int *width); +static void text_format_string_conversion(StringInfo buf, char conversion, + FmgrInfo *typOutputInfo, + Datum value, bool isNull, + int flags, int width); +static void text_format_append_string(StringInfo buf, const char *str, + int flags, int width); /***************************************************************************** @@ -3996,8 +4005,22 @@ text_reverse(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } + /* - * Returns a formated string + * Support macros for text_format() + */ +#define TEXT_FORMAT_FLAG_MINUS 0x0001 /* is minus flag present? */ + +#define ADVANCE_PARSE_POINTER(ptr,end_ptr) \ + do { \ + if (++(ptr) >= (end_ptr)) \ + ereport(ERROR, \ + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ + errmsg("unterminated format specifier"))); \ + } while (0) + +/* + * Returns a formatted string */ Datum text_format(PG_FUNCTION_ARGS) @@ -4008,16 +4031,18 @@ text_format(PG_FUNCTION_ARGS) const char *start_ptr; const char *end_ptr; text *result; - int arg = 0; + int arg; bool funcvariadic; int nargs; Datum *elements = NULL; bool *nulls = NULL; Oid element_type = InvalidOid; Oid prev_type = InvalidOid; + Oid prev_width_type = InvalidOid; FmgrInfo typoutputfinfo; + FmgrInfo typoutputinfo_width; - /* When format string is null, returns null */ + /* When format string is null, immediately return null */ if (PG_ARGISNULL(0)) PG_RETURN_NULL(); @@ -4081,10 +4106,15 @@ text_format(PG_FUNCTION_ARGS) start_ptr = VARDATA_ANY(fmt); end_ptr = start_ptr + VARSIZE_ANY_EXHDR(fmt); initStringInfo(&str); + arg = 1; /* next argument position to print */ /* Scan format string, looking for conversion specifiers. */ for (cp = start_ptr; cp < end_ptr; cp++) { + int argpos; + int widthpos; + int flags; + int width; Datum value; bool isNull; Oid typid; @@ -4099,11 +4129,7 @@ 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"))); + ADVANCE_PARSE_POINTER(cp, end_ptr); /* Easy case: %% outputs a single % */ if (*cp == '%') @@ -4112,69 +4138,89 @@ text_format(PG_FUNCTION_ARGS) continue; } + /* Parse the optional portions of the format specifier */ + cp = text_format_parse_format(cp, end_ptr, + &argpos, &widthpos, + &flags, &width); + /* - * If the user hasn't specified an argument position, we just advance - * to the next one. If they have, we must parse it. + * Next we should see the main conversion specifier. Whether or not + * an argument position was present, it's known that at least one + * character remains in the string at this point. Experience suggests + * that it's worth checking that that character is one of the expected + * ones before we try to fetch arguments, so as to produce the least + * confusing response to a mis-formatted specifier. */ - if (*cp < '0' || *cp > '9') + if (strchr("sIL", *cp) == NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized conversion type specifier \"%c\"", + *cp))); + + /* If indirect width was specified, get its value */ + if (widthpos >= 0) { - ++arg; - if (arg <= 0) /* overflow? */ - { - /* - * Should not happen, as you can't pass billions of arguments - * to a function, but better safe than sorry. - */ + /* Collect the specified or next argument position */ + if (widthpos > 0) + arg = widthpos; + if (arg >= nargs) ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("argument number is out of range"))); - } - } - else - { - bool unterminated = false; + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("too few arguments for format"))); - /* Parse digit string. */ - arg = 0; - do + /* Get the value and type of the selected argument */ + if (!funcvariadic) { - int newarg = arg * 10 + (*cp - '0'); + 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"); - 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'); + arg++; - /* - * 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; + /* We can treat NULL width the same as zero */ + if (isNull) + width = 0; + else if (typid == INT4OID) + width = DatumGetInt32(value); + else if (typid == INT2OID) + width = DatumGetInt16(value); else { - ++cp; - if (cp == end_ptr) - unterminated = true; - } - if (unterminated) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unterminated conversion specifier"))); + /* For less-usual datatypes, convert to text then to int */ + char *str; - /* 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"))); + if (typid != prev_width_type) + { + Oid typoutputfunc; + bool typIsVarlena; + + getTypeOutputInfo(typid, &typoutputfunc, &typIsVarlena); + fmgr_info(typoutputfunc, &typoutputinfo_width); + prev_width_type = typid; + } + + str = OutputFunctionCall(&typoutputinfo_width, value); + + /* pg_atoi will complain about bad data or overflow */ + width = pg_atoi(str, sizeof(int), '\0'); + + pfree(str); + } } - /* Not enough arguments? Deduct 1 to avoid counting format string. */ - if (arg > nargs - 1) + /* Collect the specified or next argument position */ + if (argpos > 0) + arg = argpos; + if (arg >= nargs) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("too few arguments for format"))); @@ -4195,6 +4241,8 @@ text_format(PG_FUNCTION_ARGS) 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 @@ -4211,9 +4259,7 @@ text_format(PG_FUNCTION_ARGS) } /* - * At this point, we should see the main conversion specifier. Whether - * or not an argument position was present, it's known that at least - * one character remains in the string at this point. + * And now we can format the value. */ switch (*cp) { @@ -4221,13 +4267,16 @@ text_format(PG_FUNCTION_ARGS) case 'I': case 'L': text_format_string_conversion(&str, *cp, &typoutputfinfo, - value, isNull); + value, isNull, + flags, width); break; default: + /* should not get here, because of previous check */ ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized conversion specifier \"%c\"", + errmsg("unrecognized conversion type specifier \"%c\"", *cp))); + break; } } @@ -4244,19 +4293,157 @@ text_format(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } -/* Format a %s, %I, or %L conversion. */ +/* + * Parse contiguous digits as a decimal number. + * + * Returns true if some digits could be parsed. + * The value is returned into *value, and *ptr is advanced to the next + * character to be parsed. + * + * Note parsing invariant: at least one character is known available before + * string end (end_ptr) at entry, and this is still true at exit. + */ +static bool +text_format_parse_digits(const char **ptr, const char *end_ptr, int *value) +{ + bool found = false; + const char *cp = *ptr; + int val = 0; + + while (*cp >= '0' && *cp <= '9') + { + int newval = val * 10 + (*cp - '0'); + + if (newval / 10 != val) /* overflow? */ + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("number is out of range"))); + val = newval; + ADVANCE_PARSE_POINTER(cp, end_ptr); + found = true; + } + + *ptr = cp; + *value = val; + + return found; +} + +/* + * Parse a format specifier (generally following the SUS printf spec). + * + * We have already advanced over the initial '%', and we are looking for + * [argpos][flags][width]type (but the type character is not consumed here). + * + * Inputs are start_ptr (the position after '%') and end_ptr (string end + 1). + * Output parameters: + * argpos: argument position for value to be printed. -1 means unspecified. + * widthpos: argument position for width. Zero means the argument position + * was unspecified (ie, take the next arg) and -1 means no width + * argument (width was omitted or specified as a constant). + * flags: bitmask of flags. + * width: directly-specified width value. Zero means the width was omitted + * (note it's not necessary to distinguish this case from an explicit + * zero width value). + * + * The function result is the next character position to be parsed, ie, the + * location where the type character is/should be. + * + * Note parsing invariant: at least one character is known available before + * string end (end_ptr) at entry, and this is still true at exit. + */ +static const char * +text_format_parse_format(const char *start_ptr, const char *end_ptr, + int *argpos, int *widthpos, + int *flags, int *width) +{ + const char *cp = start_ptr; + int n; + + /* set defaults for output parameters */ + *argpos = -1; + *widthpos = -1; + *flags = 0; + *width = 0; + + /* try to identify first number */ + if (text_format_parse_digits(&cp, end_ptr, &n)) + { + if (*cp != '$') + { + /* Must be just a width and a type, so we're done */ + *width = n; + return cp; + } + /* The number was argument position */ + *argpos = n; + /* Explicit 0 for argument index is immediately refused */ + if (n == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("format specifies argument 0, but arguments are numbered from 1"))); + ADVANCE_PARSE_POINTER(cp, end_ptr); + } + + /* Handle flags (only minus is supported now) */ + while (*cp == '-') + { + *flags |= TEXT_FORMAT_FLAG_MINUS; + ADVANCE_PARSE_POINTER(cp, end_ptr); + } + + if (*cp == '*') + { + /* Handle indirect width */ + ADVANCE_PARSE_POINTER(cp, end_ptr); + if (text_format_parse_digits(&cp, end_ptr, &n)) + { + /* number in this position must be closed by $ */ + if (*cp != '$') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("width argument position must be ended by \"$\""))); + /* The number was width argument position */ + *widthpos = n; + /* Explicit 0 for argument index is immediately refused */ + if (n == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("format specifies argument 0, but arguments are numbered from 1"))); + ADVANCE_PARSE_POINTER(cp, end_ptr); + } + else + *widthpos = 0; /* width's argument position is unspecified */ + } + else + { + /* Check for direct width specification */ + if (text_format_parse_digits(&cp, end_ptr, &n)) + *width = n; + } + + /* cp should now be pointing at type character */ + return cp; +} + +/* + * Format a %s, %I, or %L conversion + */ static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, - Datum value, bool isNull) + Datum value, bool isNull, + int flags, int width) { char *str; /* Handle NULL arguments before trying to stringify the value. */ if (isNull) { - if (conversion == 'L') - appendStringInfoString(buf, "NULL"); + 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), @@ -4271,23 +4458,71 @@ text_format_string_conversion(StringInfo buf, char conversion, if (conversion == 'I') { /* quote_identifier may or may not allocate a new string. */ - appendStringInfoString(buf, quote_identifier(str)); + text_format_append_string(buf, quote_identifier(str), flags, width); } else if (conversion == 'L') { char *qstr = quote_literal_cstr(str); - appendStringInfoString(buf, qstr); + text_format_append_string(buf, qstr, flags, width); /* quote_literal_cstr() always allocates a new string */ pfree(qstr); } else - appendStringInfoString(buf, str); + text_format_append_string(buf, str, flags, width); /* Cleanup. */ pfree(str); } +/* + * Append str to buf, padding as directed by flags/width + */ +static void +text_format_append_string(StringInfo buf, const char *str, + int flags, int width) +{ + bool align_to_left = false; + int len; + + /* fast path for typical easy case */ + if (width == 0) + { + appendStringInfoString(buf, str); + return; + } + + if (width < 0) + { + /* Negative width: implicit '-' flag, then take absolute value */ + align_to_left = true; + /* -INT_MIN is undefined */ + if (width <= INT_MIN) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("number is out of range"))); + width = -width; + } + else if (flags & TEXT_FORMAT_FLAG_MINUS) + align_to_left = true; + + len = pg_mbstrlen(str); + if (align_to_left) + { + /* left justify */ + appendStringInfoString(buf, str); + if (len < width) + appendStringInfoSpaces(buf, width - len); + } + else + { + /* right justify */ + if (len < width) + appendStringInfoSpaces(buf, width - len); + appendStringInfoString(buf, str); + } +} + /* * text_format_nv - nonvariadic wrapper for text_format function. * diff --git a/src/test/regress/expected/text.out b/src/test/regress/expected/text.out index b7565830d6..4b1c62bf53 100644 --- a/src/test/regress/expected/text.out +++ b/src/test/regress/expected/text.out @@ -209,7 +209,7 @@ ERROR: too few arguments for format select format('Hello %s'); ERROR: too few arguments for format select format('Hello %x', 20); -ERROR: unrecognized conversion specifier "x" +ERROR: unrecognized conversion type specifier "x" -- check literal and sql identifiers select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, 'Hello'); format @@ -256,12 +256,14 @@ 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('%0$s', 'Hello'); +ERROR: format specifies argument 0, but arguments are numbered from 1 +select format('%*0$s', 'Hello'); +ERROR: format specifies argument 0, but arguments are numbered from 1 select format('%1$', 1); -ERROR: unterminated conversion specifier +ERROR: unterminated format specifier select format('%1$1', 1); -ERROR: unrecognized conversion specifier "1" +ERROR: unterminated format specifier -- check mix of positional and ordered placeholders select format('Hello %s %1$s %s', 'World', 'Hello again'); format @@ -328,3 +330,106 @@ from generate_series(1,200) g(i); 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) +-- check field widths and left, right alignment +select format('>>%10s<<', 'Hello'); + format +---------------- + >> Hello<< +(1 row) + +select format('>>%10s<<', NULL); + format +---------------- + >> << +(1 row) + +select format('>>%10s<<', ''); + format +---------------- + >> << +(1 row) + +select format('>>%-10s<<', ''); + format +---------------- + >> << +(1 row) + +select format('>>%-10s<<', 'Hello'); + format +---------------- + >>Hello << +(1 row) + +select format('>>%-10s<<', NULL); + format +---------------- + >> << +(1 row) + +select format('>>%1$10s<<', 'Hello'); + format +---------------- + >> Hello<< +(1 row) + +select format('>>%1$-10I<<', 'Hello'); + format +---------------- + >>"Hello" << +(1 row) + +select format('>>%2$*1$L<<', 10, 'Hello'); + format +---------------- + >> 'Hello'<< +(1 row) + +select format('>>%2$*1$L<<', 10, NULL); + format +---------------- + >> NULL<< +(1 row) + +select format('>>%2$*1$L<<', -10, NULL); + format +---------------- + >>NULL << +(1 row) + +select format('>>%*s<<', 10, 'Hello'); + format +---------------- + >> Hello<< +(1 row) + +select format('>>%*1$s<<', 10, 'Hello'); + format +---------------- + >> Hello<< +(1 row) + +select format('>>%-s<<', 'Hello'); + format +----------- + >>Hello<< +(1 row) + +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) + diff --git a/src/test/regress/sql/text.sql b/src/test/regress/sql/text.sql index a96e9f7d1e..c4ed74b39d 100644 --- a/src/test/regress/sql/text.sql +++ b/src/test/regress/sql/text.sql @@ -78,7 +78,8 @@ select format('%1$s %12$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); -- 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('%1s', 1); +select format('%0$s', 'Hello'); +select format('%*0$s', 'Hello'); select format('%1$', 1); select format('%1$1', 1); -- check mix of positional and ordered placeholders @@ -97,3 +98,21 @@ select format('Hello', variadic NULL); -- 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); +-- check field widths and left, right alignment +select format('>>%10s<<', 'Hello'); +select format('>>%10s<<', NULL); +select format('>>%10s<<', ''); +select format('>>%-10s<<', ''); +select format('>>%-10s<<', 'Hello'); +select format('>>%-10s<<', NULL); +select format('>>%1$10s<<', 'Hello'); +select format('>>%1$-10I<<', 'Hello'); +select format('>>%2$*1$L<<', 10, 'Hello'); +select format('>>%2$*1$L<<', 10, NULL); +select format('>>%2$*1$L<<', -10, NULL); +select format('>>%*s<<', 10, 'Hello'); +select format('>>%*1$s<<', 10, 'Hello'); +select format('>>%-s<<', 'Hello'); +select format('>>%10L<<', NULL); +select format('>>%2$*1$L<<', NULL, 'Hello'); +select format('>>%2$*1$L<<', 0, 'Hello'); -- GitLab