*** 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, bool use_width, 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); *************** *** 3997,4002 **** text_reverse(PG_FUNCTION_ARGS) --- 3998,4134 ---- } /* + * Returns pointer of first non digit char + * + * When some digits are processed, then inum returns parsed number + * and is_valid is true, otherwise is_valid is false. + */ + static const char * + text_format_parse_digits(const char *start_ptr, const char *end_ptr, + int *inum, /* OUT param */ + bool *is_valid) /* OUT param */ + { + const char *cp = start_ptr; + + *is_valid = false; + *inum = 0; + + /* continue, only when start_ptr is less than end_ptr */ + while (cp < end_ptr) + { + if (*cp >= '0' && *cp <= '9') + { + int newnum = *inum * 10 + (*cp - '0'); + + if (newnum / 10 != *inum) /* overflow? */ + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("number is out of range"))); + *inum = newnum; + *is_valid = true; + ++cp; + } + else + break; + } + + /* should not be after last char of format string */ + if (cp >= end_ptr) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unterminated conversion specifier"))); + + return cp; + } + + #define TEXT_FORMAT_FLAG_MINUS 0x0001 /* is minus in format string? */ + + #define TEXT_FORMAT_NEXT_CHAR(ptr, end_ptr) \ + do { \ + if (++(ptr) >= (end_ptr)) \ + ereport(ERROR, \ + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ + errmsg("unterminated conversion specifier"))); \ + } while (0) + + /* + * parse format specification + * [parameter][flags][width]type + * + * -1 in int OUT parameters signalize so this attribut is missing, + * all values should be zero or greather than zero. Raise exeption + * when some parameter is out of range. + */ + static const char * + text_format_parse_format(const char *start_ptr, const char *end_ptr, + int *parameter, /* OUT param */ + int *flags, /* OUT param */ + int *width, /* OUT param */ + bool *indirect_width, /* OUT param */ + int *indirect_width_parameter) /* OUT param */ + { + int inum; + bool is_valid; + + /* set defaults to out parameters */ + *parameter = -1; + *flags = 0; + *width = -1; + *indirect_width = false; + *indirect_width_parameter = -1; + + /* try to identify first number */ + start_ptr = text_format_parse_digits(start_ptr, end_ptr, &inum, &is_valid); + if (is_valid) + { + /* number on this position can be parameter number or the width */ + if (*start_ptr != '$') + { + *width = inum; + return start_ptr; + } + + *parameter = inum; + TEXT_FORMAT_NEXT_CHAR(start_ptr, end_ptr); + } + + /* try to parse flags, only minus is supported now */ + if (*start_ptr == '-') + { + *flags = *flags | TEXT_FORMAT_FLAG_MINUS; + TEXT_FORMAT_NEXT_CHAR(start_ptr, end_ptr); + } + + /* try to parse indirect width */ + if (*start_ptr == '*') + { + start_ptr = text_format_parse_digits(++start_ptr, end_ptr, &inum, &is_valid); + if (is_valid) + { + /* number in this position should be closed by $ */ + if (*start_ptr != '$') + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unexpected char \"%c\"", *start_ptr))); + TEXT_FORMAT_NEXT_CHAR(start_ptr, end_ptr); + + *indirect_width_parameter = inum; + } + else + *indirect_width = true; + + return start_ptr; + } + + /* last possible nummber - width */ + start_ptr = text_format_parse_digits(start_ptr, end_ptr, &inum, &is_valid); + if (is_valid) + *width = inum; + + return start_ptr; + } + + /* * Returns a formated string */ Datum *************** *** 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)) *************** *** 4088,4093 **** text_format(PG_FUNCTION_ARGS) --- 4222,4233 ---- Datum value; bool isNull; Oid typid; + int parameter; + int flags; + int width; + bool indirect_width; + int indirect_width_parameter; + bool use_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 == '%') --- 4239,4246 ---- continue; } ! /* go to next char */ ! TEXT_FORMAT_NEXT_CHAR(cp, end_ptr); /* Easy case: %% outputs a single % */ if (*cp == '%') *************** *** 4112,4125 **** 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 --- 4249,4264 ---- continue; } ! cp = text_format_parse_format(cp, end_ptr, ! ¶meter, ! &flags, ! &width, ! &indirect_width, ! &indirect_width_parameter); ! ! if (indirect_width) { ! if (++arg <= 0) /* overflow? */ { /* * Should not happen, as you can't pass billions of arguments *************** *** 4129,4177 **** text_format(PG_FUNCTION_ARGS) (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) --- 4268,4301 ---- (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("argument number is out of range"))); } + indirect_width_parameter = arg; } ! else if (indirect_width_parameter != -1) { ! /* be consistent, move ordered argument together with positional */ ! arg = indirect_width_parameter; ! } ! if (parameter == -1) ! { ! 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 + arg = parameter; + + 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) *************** *** 4179,4184 **** text_format(PG_FUNCTION_ARGS) --- 4303,4380 ---- (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("too few arguments for format"))); + if (indirect_width_parameter != -1) + { + + if (indirect_width_parameter == 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 (indirect_width_parameter > nargs - 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("too few arguments for format"))); + + /* get value of related parameter that holds width and coerce to int */ + if (!funcvariadic) + { + value = PG_GETARG_DATUM(indirect_width_parameter); + isNull = PG_ARGISNULL(indirect_width_parameter); + typid = get_fn_expr_argtype(fcinfo->flinfo, indirect_width_parameter); + } + else + { + value = elements[indirect_width_parameter - 1]; + isNull = nulls[indirect_width_parameter - 1]; + typid = element_type; + } + if (!OidIsValid(typid)) + elog(ERROR, "could not determine data type of format() input"); + + /* + * we don't need to different between NULL and zero in this moment, + * NULL means ignore this width - same as zero. + */ + if (isNull) + { + value = 0; + isNull = false; + typid = INT4OID; + } + else if (typid != INT4OID) + { + 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); + } + else + width = DatumGetInt32(value); + + use_width = true; + } + else if (width != -1) + use_width = true; + else + use_width = false; + /* Get the value and type of the selected argument */ if (!funcvariadic) { *************** *** 4221,4227 **** text_format(PG_FUNCTION_ARGS) case 'I': case 'L': text_format_string_conversion(&str, *cp, &typoutputfinfo, ! value, isNull); break; default: ereport(ERROR, --- 4417,4424 ---- case 'I': case 'L': text_format_string_conversion(&str, *cp, &typoutputfinfo, ! value, isNull, ! flags, use_width, width); break; default: ereport(ERROR, *************** *** 4244,4254 **** 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; --- 4441,4497 ---- 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, bool use_width, int width) + { + bool align_to_left; + int len; + + /* fast path */ + if (!use_width) + { + appendStringInfoString(buf, str); + return; + } + + if (width < 0) + { + width = -width; + align_to_left = true; + } + else if (flags & TEXT_FORMAT_FLAG_MINUS) + { + align_to_left = true; + } + else + align_to_left = false; + + 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, bool use_width, int width) { char *str; *************** *** 4256,4262 **** text_format_string_conversion(StringInfo buf, char conversion, if (isNull) { if (conversion == 'L') ! appendStringInfoString(buf, "NULL"); else if (conversion == 'I') ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), --- 4499,4506 ---- if (isNull) { if (conversion == 'L') ! text_format_append_string(buf, "NULL", ! flags, use_width, width); else if (conversion == 'I') ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), *************** *** 4271,4288 **** 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)); } 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); --- 4515,4534 ---- if (conversion == 'I') { /* quote_identifier may or may not allocate a new string. */ ! text_format_append_string(buf, quote_identifier(str), ! flags, use_width, width); } else if (conversion == 'L') ! { char *qstr = quote_literal_cstr(str); ! text_format_append_string(buf, qstr, ! flags, use_width, width); /* quote_literal_cstr() always allocates a new string */ pfree(qstr); } else ! text_format_append_string(buf, str, ! flags, use_width, width); /* Cleanup. */ pfree(str); *** a/src/test/regress/expected/text.out --- b/src/test/regress/expected/text.out *************** *** 257,267 **** 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 --- 257,271 ---- 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); ! 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); --- 332,387 ---- 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<<', 'Hello') + 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('>>%*s<<', 10, 'Hello'); + format + ---------------- + >> Hello<< + >>Hello << + >> Hello<< + >>"Hello" << + >> 'Hello'<< + >> Hello<< + (6 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 *************** *** 97,99 **** select format('Hello', variadic NULL); --- 97,120 ---- -- 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<<', 'Hello') + 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('>>%*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');