*** a/doc/src/sgml/func.sgml --- b/doc/src/sgml/func.sgml *************** *** 1530,1536 **** 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 . --- 1530,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. 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 *************** *** 77,83 **** static bytea *bytea_substring(Datum str, static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl); static StringInfo makeStringAggState(FunctionCallInfo fcinfo); void text_format_string_conversion(StringInfo buf, char conversion, ! Oid typid, Datum value, bool isNull); static Datum text_to_array_internal(PG_FUNCTION_ARGS); static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, --- 77,84 ---- static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl); static StringInfo makeStringAggState(FunctionCallInfo fcinfo); void text_format_string_conversion(StringInfo buf, char conversion, ! Oid typid, Datum value, bool isNull, ! bool with_width, int width); static Datum text_to_array_internal(PG_FUNCTION_ARGS); static text *array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, *************** *** 3947,3952 **** text_reverse(PG_FUNCTION_ARGS) --- 3948,4028 ---- } /* + * 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 *************** *** 3959,3965 **** text_format(PG_FUNCTION_ARGS) const char *end_ptr; text *result; int arg; ! int position = 0; bool positional = false; bool ordered = false; bool warning_emmited = false; --- 4035,4041 ---- const char *end_ptr; text *result; int arg; ! int ordered_nargs = 0; bool positional = false; bool ordered = false; bool warning_emmited = false; *************** *** 3980,3985 **** text_format(PG_FUNCTION_ARGS) --- 4056,4063 ---- Datum value; bool isNull; Oid typid; + int width = 0; + bool with_width = false; /* * If it's not the start of a conversion specifier, just copy it to *************** *** 4004,4025 **** 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 --- 4082,4125 ---- 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"))); ! ! with_width = true; ! } } ! else ! { ! /* previous digits was a width not argument number */ ! width = arg; ! arg = 0; ! with_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 *************** *** 4029,4084 **** 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. */ --- 4129,4154 ---- (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. */ *************** *** 4099,4108 **** text_format(PG_FUNCTION_ARGS) switch (*cp) { case 's': case 'I': case 'L': ! text_format_string_conversion(&str, *cp, typid, value, isNull); break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), --- 4169,4186 ---- switch (*cp) { case 's': + text_format_string_conversion(&str, *cp, typid, value, isNull, with_width, width); + break; + case 'I': case 'L': ! if (with_width) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_PARAMETER_VALUE), ! errmsg("conversion \"%c\" doesn't suppport the width", *cp))); ! text_format_string_conversion(&str, *cp, typid, value, isNull, false, 0); break; + default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), *************** *** 4121,4127 **** text_format(PG_FUNCTION_ARGS) /* Format a %s, %I, or %L conversion. */ void text_format_string_conversion(StringInfo buf, char conversion, ! Oid typid, Datum value, bool isNull) { Oid typOutput; bool typIsVarlena; --- 4199,4206 ---- /* Format a %s, %I, or %L conversion. */ void text_format_string_conversion(StringInfo buf, char conversion, ! Oid typid, Datum value, bool isNull, ! bool with_width, int width) { Oid typOutput; bool typIsVarlena; *************** *** 4158,4164 **** text_format_string_conversion(StringInfo buf, char conversion, pfree(qstr); } else ! appendStringInfoString(buf, str); /* Cleanup. */ pfree(str); --- 4237,4266 ---- pfree(qstr); } else ! { ! /* fast path */ ! if (!with_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 *************** *** 222,233 **** 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" --checkk mix of positional and ordered placeholders --should raise warnings select format('Hello %s %1$s %s', 'World', 'Hello again'); --- 222,231 ---- 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 --checkk mix of positional and ordered placeholders --should raise warnings select format('Hello %s %1$s %s', 'World', 'Hello again'); *************** *** 251,253 **** WARNING: ordered and positional placeholders are used together --- 249,283 ---- Hello World Hello again Hello again (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 suppport the width + select format('>>%1$10L<<', 'tablename'); + ERROR: conversion "L" doesn't suppport the width *** a/src/test/regress/sql/text.sql --- b/src/test/regress/sql/text.sql *************** *** 70,76 **** 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); --checkk mix of positional and ordered placeholders --- 70,75 ---- *************** *** 78,80 **** select format('%1$1', 1); --- 77,90 ---- select format('Hello %s %1$s %s', 'World', 'Hello again'); select format('Hello %s %s, %2$s %2$s', 'World', 'Hello again'); select format('Hello %s %2$s %s', 'World', 'Hello again'); + + -- 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');