*** a/doc/src/sgml/func.sgml --- b/doc/src/sgml/func.sgml *************** *** 1532,1538 **** 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. A warnig is raised when positional and ordered placeholders are used together. See also . --- 1532,1540 ---- 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. Similary to C ! function sprintf; is possible to specify a width for ! %s conversion. A warnig is raised when positional and ordered placeholders are used together. See also . *** 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, ! 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); *************** *** 3984,3989 **** text_reverse(PG_FUNCTION_ARGS) --- 3985,4065 ---- } /* + * Returns ptr of first non digit char. Raise error, + * when no digit is processed or when result of parsing is not number + */ + static const char * + parse_digit_subformat(const char *start_ptr, const char *end_ptr, int *value) + { + const char *cp = start_ptr; + bool minus = false; + int inum = 0; + + /* continue, only when start_ptr is less than end_ptr */ + if (cp < end_ptr) + { + if (*cp == '-') + { + minus = true; + start_ptr = cp++; + } + + 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; + ++cp; + } + else + break; + } + + *value = minus ? - inum : inum; + } + + /* digit cannot be last char */ + if (cp >= end_ptr) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unterminated conversion specifier"))); + + /* we call this routine, only when we expected number */ + if (cp == start_ptr) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("invalid format, missing expected number"))); + + /* argument number must by greather than zero */ + if (*cp == '$') + { + if (minus) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("conversion specifies argument 0, but arguments are numbered from 1"))); + + if (inum == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("conversion specifies argument 0, but arguments are numbered from 1"))); + + /* $ must not be last char */ + if (cp + 1 >= end_ptr) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unterminated conversion specifier"))); + } + + return cp; + } + + /* * Returns a formated string */ Datum *************** *** 4003,4009 **** text_format(PG_FUNCTION_ARGS) Oid element_type = InvalidOid; Oid prev_type = InvalidOid; FmgrInfo typoutputfinfo; ! int position = 0; bool positional = false; bool ordered = false; bool warning_emmited = false; --- 4079,4085 ---- Oid element_type = InvalidOid; Oid prev_type = InvalidOid; FmgrInfo typoutputfinfo; ! int ordered_nargs = 0; bool positional = false; bool ordered = false; bool warning_emmited = false; *************** *** 4066,4071 **** text_format(PG_FUNCTION_ARGS) --- 4142,4149 ---- Datum value; bool isNull; Oid typid; + int width = 0; + bool use_width = false; /* * If it's not the start of a conversion specifier, just copy it to *************** *** 4090,4111 **** 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') { ! /* don't allow mix styles - reflects glibc behave */ ! if (positional && !warning_emmited) { ! elog(WARNING, "ordered and positional placeholders are used together"); ! warning_emmited = true; } ! ordered = true; ! ++position; ! if (position <= 0) /* overflow? */ { /* * Should not happen, as you can't pass billions of arguments --- 4168,4211 ---- continue; } + arg = 0; /* ! * User can specify a width or a position or both. The width should ! * be negative. */ ! if (*cp == '-' || (*cp >= '0' && *cp <= '9')) { ! cp = parse_digit_subformat(cp, end_ptr, &arg); ! ! if (*cp == '$') { ! ++cp; ! if (*cp == '-' || (*cp >= '0' && *cp <= '9')) ! { ! cp = parse_digit_subformat(cp, end_ptr, &width); ! ! if (*cp == '$') ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ! errmsg("argument number is defined yet"))); ! ! use_width = true; ! } } ! else ! { ! /* previous digits was a width not argument number */ ! width = arg; ! arg = 0; ! use_width = true; ! } ! } ! /* when argument not specified yet, use a next one */ ! if (arg == 0) ! { ! ++ordered_nargs; ! if (ordered_nargs <= 0) /* overflow? */ { /* * Should not happen, as you can't pass billions of arguments *************** *** 4115,4170 **** text_format(PG_FUNCTION_ARGS) (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("argument number is out of range"))); } ! arg = position; ! } ! 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"))); if (ordered && !warning_emmited) { elog(WARNING, "ordered and positional placeholders are used together"); warning_emmited = true; } - positional = true; ! /* 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. */ --- 4215,4240 ---- (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("argument number is out of range"))); } ! arg = ordered_nargs; ! /* raise warning when found mixed placeholders, but only once */ ! if (positional && !warning_emmited) { ! elog(WARNING, "ordered and positional placeholders are used together"); ! warning_emmited = true; } + ordered = true; + } + else + { if (ordered && !warning_emmited) { elog(WARNING, "ordered and positional placeholders are used together"); warning_emmited = true; } ! positional = true; } /* Not enough arguments? Deduct 1 to avoid counting format string. */ *************** *** 4212,4221 **** text_format(PG_FUNCTION_ARGS) switch (*cp) { case 's': case 'I': case 'L': text_format_string_conversion(&str, *cp, &typoutputfinfo, ! value, isNull); break; default: ereport(ERROR, --- 4282,4301 ---- switch (*cp) { case 's': + text_format_string_conversion(&str, *cp, &typoutputfinfo, + value, isNull, + use_width, width); + break; + case 'I': case 'L': + if (use_width) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("conversion \"%c\" doesn't support the width specification", *cp))); text_format_string_conversion(&str, *cp, &typoutputfinfo, ! value, isNull, ! false, 0); break; default: ereport(ERROR, *************** *** 4242,4248 **** text_format(PG_FUNCTION_ARGS) static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, ! Datum value, bool isNull) { char *str; --- 4322,4329 ---- static void text_format_string_conversion(StringInfo buf, char conversion, FmgrInfo *typOutputInfo, ! Datum value, bool isNull, ! bool use_width, int width) { char *str; *************** *** 4276,4282 **** text_format_string_conversion(StringInfo buf, char conversion, pfree(qstr); } else ! appendStringInfoString(buf, str); /* Cleanup. */ pfree(str); --- 4357,4386 ---- pfree(qstr); } else ! { ! /* fast path */ ! if (!use_width) ! appendStringInfoString(buf, str); ! else ! { ! int len = pg_mbstrlen(str); ! ! if (width < 0) ! { ! /* allign to left */ ! appendStringInfoString(buf, str); ! if (len < (-width)) ! appendStringInfoSpaces(buf, - (len + width)); ! } ! else ! { ! /* allign to right */ ! if (len < width) ! appendStringInfoSpaces(buf, width - len); ! appendStringInfoString(buf, str); ! } ! } ! } /* Cleanup. */ pfree(str); *** a/src/test/regress/expected/text.out --- b/src/test/regress/expected/text.out *************** *** 258,269 **** 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 -- should raise warning select format('Hello %s %1$s %s', 'World', 'Hello again'); --- 258,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('%1$', 1); ERROR: unterminated conversion specifier select format('%1$1', 1); ! ERROR: unterminated conversion specifier -- check mix of positional and ordered placeholders -- should raise warning select format('Hello %s %1$s %s', 'World', 'Hello again'); *************** *** 340,342 **** from generate_series(1,200) g(i); --- 338,372 ---- 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) + -- format with a width + select format('>>%10s<<', 'Hello'); + format + ---------------- + >> Hello<< + (1 row) + + select format('>>%-10s<<', 'Hello'); + format + ---------------- + >>Hello << + (1 row) + + select format('>>%1$10s<<', 'Hello'); + format + ---------------- + >> Hello<< + (1 row) + + select format('>>%1$-10s<<', 'Hello'); + format + ---------------- + >>Hello << + (1 row) + + -- should fail + select format('>>%1$1$s<<', 'Hello'); + ERROR: argument number is defined yet + select format('>>%10I<<', 'tablename'); + ERROR: conversion "I" doesn't support the width specification + select format('>>%1$10L<<', 'tablename'); + ERROR: conversion "L" doesn't support the width specification *** a/src/test/regress/sql/text.sql --- b/src/test/regress/sql/text.sql *************** *** 78,84 **** 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('%1$', 1); select format('%1$1', 1); -- check mix of positional and ordered placeholders --- 78,83 ---- *************** *** 99,101 **** select format('Hello', variadic NULL::int[]); --- 98,112 ---- -- 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); + + + -- format with a width + select format('>>%10s<<', 'Hello'); + select format('>>%-10s<<', 'Hello'); + select format('>>%1$10s<<', 'Hello'); + select format('>>%1$-10s<<', 'Hello'); + + -- should fail + select format('>>%1$1$s<<', 'Hello'); + select format('>>%10I<<', 'tablename'); + select format('>>%1$10L<<', 'tablename');