Hello I reworked a implementation of format function. This respects last discussion:
* support a positional placeholders - syntax %99$x, * support a tags: %s, I, L, * enhanced documentation, * enhanced reggress tests Regards Pavel Stehule
*** ./doc/src/sgml/func.sgml.orig 2010-10-21 14:40:44.000000000 +0200 --- ./doc/src/sgml/func.sgml 2010-10-22 10:24:18.764199680 +0200 *************** *** 1272,1277 **** --- 1272,1280 ---- <primary>encode</primary> </indexterm> <indexterm> + <primary>format</primary> + </indexterm> + <indexterm> <primary>initcap</primary> </indexterm> <indexterm> *************** *** 1497,1502 **** --- 1500,1524 ---- </row> <row> + <entry> + <literal><function>format</function>(<parameter>formatstr</parameter> <type>text</type> + [, <parameter>str</parameter> <type>"any"</type> [, ...] ])</literal> + </entry> + <entry><type>text</type></entry> + <entry> + This functions can be used to create a formated string or message. There are allowed + three types of tags: %s as string, %I as SQL identifiers and %L as SQL literals. Attention: + result for %I and %L must not be same as result of <function>quote_ident</function> and + <function>quote_literal</function> functions, because this function doesn't try to coerce + parameters to <type>text</type> type and directly use a type's output functions. The placeholder + can be related to some explicit parameter with using a optional n$ specification inside format. + See also <xref linkend="plpgsql-quote-literal-example">. + </entry> + <entry><literal>format('Hello %s, %1$s', 'World')</literal></entry> + <entry><literal>Hello World, World</literal></entry> + </row> + + <row> <entry><literal><function>initcap(<parameter>string</parameter>)</function></literal></entry> <entry><type>text</type></entry> <entry> *** ./doc/src/sgml/plpgsql.sgml.orig 2010-10-11 08:33:15.000000000 +0200 --- ./doc/src/sgml/plpgsql.sgml 2010-10-22 10:37:06.041200330 +0200 *************** *** 1250,1255 **** --- 1250,1273 ---- <emphasis>must</> use <function>quote_literal</>, <function>quote_nullable</>, or <function>quote_ident</>, as appropriate. </para> + + <para> + Building a string with dynamic SQL statement can be simplified + with using a <function>format</function> function (see <xref + linkend="functions-string">): + <programlisting> + EXECUTE format('UPDATE tbl SET %I = %L WHERE key = %L', colname, newvalue, keyvalue); + </programlisting> + A use the <function>format</function> format can be together with + <literal>USING</literal> clause: + <programlisting> + EXECUTE format('UPDATE tbl SET %I = $1 WHERE key = $2', colname) + USING newvalue, keyvalue; + </programlisting> + This form is little bit more efective, because a parameters + <literal>newvalue</literal> and <literal>keyvalue</literal> must not be + converted to text. + </para> </example> <para> *** ./src/backend/utils/adt/varlena.c.orig 2010-10-11 08:33:15.000000000 +0200 --- ./src/backend/utils/adt/varlena.c 2010-10-22 09:46:18.863075117 +0200 *************** *** 3702,3704 **** --- 3702,3939 ---- PG_RETURN_TEXT_P(result); } + + typedef struct { + char field_type; + int32 position; + bool is_positional; + } placeholder_desc; + + #define INVALID_PARAMETER_ERROR(d) ereport(ERROR, \ + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ + errmsg("invalid formatting string for function format"), \ + errdetail(d))) + + static const char * + parse_format(placeholder_desc *pd, const char *start_ptr, + const char *end_ptr, int nparams) + { + bool is_valid = false; + + pd->is_positional = false; + + while (start_ptr <= end_ptr) + { + switch (*start_ptr) + { + case 's': + pd->field_type = 's'; + is_valid = true; + break; + + case 'I': + pd->field_type = 'I'; + is_valid = true; + break; + + case 'L': + pd->field_type = 'L'; + is_valid = true; + break; + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if (pd->is_positional) + INVALID_PARAMETER_ERROR("positional parameter is defined already"); + + pd->position = *start_ptr - '0'; + while (start_ptr < end_ptr && isdigit(start_ptr[1])) + { + pd->position = pd->position * 10 + *++start_ptr - '0'; + /* don't allow to integer overflow */ + if (pd->position >= nparams) + INVALID_PARAMETER_ERROR("placeholder is out of range"); + } + + /* recheck position, or first check for one digit positions */ + if (pd->position >= nparams) + INVALID_PARAMETER_ERROR("placeholder is out of range"); + + /* next character must be a $ */ + if (start_ptr == end_ptr && isdigit(*start_ptr)) + INVALID_PARAMETER_ERROR("unexpected end, missing '$'"); + + if (*++start_ptr != '$') + INVALID_PARAMETER_ERROR("expected '$'"); + start_ptr++; + + pd->is_positional = true; + break; + + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid formatting string for function format"), + errdetail("unsupported tag \"%%%.*s\"", pg_mblen(start_ptr), start_ptr))); + } + + if (is_valid) + break; + } + + if (!is_valid) + INVALID_PARAMETER_ERROR("missing field type"); + + return start_ptr; + } + + /* + * Returns a formated string + */ + Datum + text_format(PG_FUNCTION_ARGS) + { + text *fmt; + StringInfoData str; + const char *cp; + int i = 1; + size_t len; + const char *start_ptr; + const char *end_ptr; + text *result; + bool only_ordered_parameters = true; + + /* When format string is null, returns null */ + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + fmt = PG_GETARG_TEXT_PP(0); + len = VARSIZE_ANY_EXHDR(fmt); + start_ptr = VARDATA_ANY(fmt); + end_ptr = start_ptr + len - 1; + + initStringInfo(&str); + for (cp = start_ptr; cp <= end_ptr; cp++) + { + if (cp[0] == '%') + { + placeholder_desc pd = { ' ', 0, false }; /* be compiler quiet */ + Oid valtype; + Datum value; + bool isNull; + int arg_num; + Oid typoutput; + bool typIsVarlena; + + /* initial check */ + if (cp == end_ptr) + INVALID_PARAMETER_ERROR("missing field type"); + + /* replace escaped symbol */ + if (cp[1] == '%') + { + appendStringInfoChar(&str, '%'); + cp++; + continue; + } + + cp = parse_format(&pd, ++cp, end_ptr, PG_NARGS()); + + if (pd.is_positional) + { + arg_num = pd.position; + only_ordered_parameters = false; + } + else + { + /* ordered arguments are not checked yet */ + if (i >= PG_NARGS()) + INVALID_PARAMETER_ERROR("too few parameters"); + arg_num = i++; + } + + value = PG_GETARG_DATUM(arg_num); + isNull = PG_ARGISNULL(arg_num); + valtype = get_fn_expr_argtype(fcinfo->flinfo, arg_num); + getTypeOutputInfo(valtype, &typoutput, &typIsVarlena); + + switch (pd.field_type) + { + case 's': + { + if (!isNull) + { + char *outstr = OidOutputFunctionCall(typoutput, value); + + appendStringInfoString(&str, outstr); + pfree(outstr); + } + + break; + } + + case 'I': + { + char *target_value; + + if (isNull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("SQL identifier is undefined"), + errhint("You try to use NULL for %%I format"))); + + /* show it as sql identifier */ + target_value = OidOutputFunctionCall(typoutput, value); + appendStringInfoString(&str, quote_identifier(target_value)); + pfree(target_value); + + break; + } + + case 'L': + { + text *txt; + text *quoted_txt; + + if (!isNull) + { + /* get text value and quotize */ + txt = cstring_to_text(OidOutputFunctionCall(typoutput, value)); + quoted_txt = DatumGetTextP(DirectFunctionCall1(quote_literal, + PointerGetDatum(txt))); + appendStringInfoString(&str, text_to_cstring(quoted_txt)); + + pfree(quoted_txt); + pfree(txt); + } + else + appendStringInfoString(&str, "NULL"); + + break; + } + + default: + /* not possible */ + elog(ERROR, "unexpected field type"); + } + } + else + appendStringInfoChar(&str, cp[0]); + } + + result = cstring_to_text_with_len(str.data, str.len); + pfree(str.data); + + PG_RETURN_TEXT_P(result); + } + + /* + * text_format_nv - nonvariadic wrapper for text_format function. + * + * note: this wrapper is necessary to be sanity_checks test ok + */ + Datum + text_format_nv(PG_FUNCTION_ARGS) + { + return text_format(fcinfo); + } *** ./src/include/catalog/pg_proc.h.orig 2010-10-11 08:33:15.000000000 +0200 --- ./src/include/catalog/pg_proc.h 2010-10-22 09:17:51.410199979 +0200 *************** *** 2741,2746 **** --- 2741,2750 ---- DESCR("return the last n characters"); DATA(insert OID = 3062 ( reverse PGNSP PGUID 12 1 0 0 f f f t f i 1 0 25 "25" _null_ _null_ _null_ _null_ text_reverse _null_ _null_ _null_ )); DESCR("reverse text"); + DATA(insert OID = 3063 ( format PGNSP PGUID 12 1 0 2276 f f f f f s 2 0 25 "25 2276" "{25,2276}" "{i,v}" _null_ _null_ text_format _null_ _null_ _null_ )); + DESCR("format text message"); + DATA(insert OID = 3064 ( format PGNSP PGUID 12 1 0 0 f f f f f s 1 0 25 "25" _null_ _null_ _null_ _null_ text_format_nv _null_ _null_ _null_ )); + DESCR("format text message"); DATA(insert OID = 1810 ( bit_length PGNSP PGUID 14 1 0 0 f f f t f i 1 0 23 "17" _null_ _null_ _null_ _null_ "select pg_catalog.octet_length($1) * 8" _null_ _null_ _null_ )); DESCR("length in bits"); *** ./src/include/utils/builtins.h.orig 2010-10-11 08:33:15.000000000 +0200 --- ./src/include/utils/builtins.h 2010-10-22 08:49:28.642077134 +0200 *************** *** 742,747 **** --- 742,749 ---- extern Datum text_left(PG_FUNCTION_ARGS); extern Datum text_right(PG_FUNCTION_ARGS); extern Datum text_reverse(PG_FUNCTION_ARGS); + extern Datum text_format(PG_FUNCTION_ARGS); + extern Datum text_format_nv(PG_FUNCTION_ARGS); /* version.c */ extern Datum pgsql_version(PG_FUNCTION_ARGS); *** ./src/test/regress/expected/text.out.orig 2010-10-11 08:33:15.000000000 +0200 --- ./src/test/regress/expected/text.out 2010-10-22 10:07:47.000000000 +0200 *************** *** 118,120 **** --- 118,231 ---- 5 | ahoj | ahoj (11 rows) + select format(NULL); + format + -------- + + (1 row) + + select format('Hello'); + format + -------- + Hello + (1 row) + + select format('Hello %s', 'World'); + format + ------------- + Hello World + (1 row) + + select format('Hello %%'); + format + --------- + Hello % + (1 row) + + select format('Hello %%%%'); + format + ---------- + Hello %% + (1 row) + + -- should fail + select format('Hello %s %s', 'World'); + ERROR: invalid formatting string for function format + DETAIL: too few parameters + select format('Hello %s'); + ERROR: invalid formatting string for function format + DETAIL: too few parameters + select format('Hello %x', 20); + ERROR: invalid formatting string for function format + DETAIL: unsupported tag "%x" + -- check literal and sql identifiers + select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, 'Hello'); + format + ---------------------------------------- + INSERT INTO mytab VALUES('10','Hello') + (1 row) + + select format('%s%s%s','Hello', NULL,'World'); + format + ------------ + HelloWorld + (1 row) + + select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, NULL); + format + ------------------------------------- + INSERT INTO mytab VALUES('10',NULL) + (1 row) + + select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', NULL, 'Hello'); + format + ---------------------------------------- + INSERT INTO mytab VALUES(NULL,'Hello') + (1 row) + + -- should fail, sql identifier cannot be NULL + select format('INSERT INTO %I VALUES(%L,%L)', NULL, 10, 'Hello'); + ERROR: SQL identifier is undefined + HINT: You try to use NULL for %I format + -- check positional placeholders + select format('%1$s %3$s', 1, 2, 3); + format + -------- + 1 3 + (1 row) + + select format('%1$s %12$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + format + -------- + 1 12 + (1 row) + + -- should fail + select format('%1$s %4$s', 1, 2, 3); + ERROR: invalid formatting string for function format + DETAIL: placeholder is out of range + select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + ERROR: invalid formatting string for function format + DETAIL: placeholder is out of range + select format('%1s', 1); + ERROR: invalid formatting string for function format + DETAIL: expected '$' + select format('%1$', 1); + ERROR: invalid formatting string for function format + DETAIL: missing field type + select format('%1$1', 1); + ERROR: invalid formatting string for function format + DETAIL: positional parameter is defined already + --checkk mix of positional and ordered placeholders + select format('Hello %s %1$s %s', 'World', 'Hello again'); + format + ------------------------------- + Hello World World Hello again + (1 row) + + select format('Hello %s %s, %2$s %2$s', 'World', 'Hello again'); + format + -------------------------------------------------- + Hello World Hello again, Hello again Hello again + (1 row) + *** ./src/test/regress/sql/text.sql.orig 2010-10-11 08:33:15.000000000 +0200 --- ./src/test/regress/sql/text.sql 2010-10-22 10:41:36.369200039 +0200 *************** *** 41,43 **** --- 41,72 ---- select concat_ws(NULL,10,20,null,30) is null; select reverse('abcde'); select i, left('ahoj', i), right('ahoj', i) from generate_series(-5, 5) t(i) order by i; + + select format(NULL); + select format('Hello'); + select format('Hello %s', 'World'); + select format('Hello %%'); + select format('Hello %%%%'); + -- should fail + select format('Hello %s %s', 'World'); + select format('Hello %s'); + select format('Hello %x', 20); + -- check literal and sql identifiers + select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, 'Hello'); + select format('%s%s%s','Hello', NULL,'World'); + select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', 10, NULL); + select format('INSERT INTO %I VALUES(%L,%L)', 'mytab', NULL, 'Hello'); + -- should fail, sql identifier cannot be NULL + select format('INSERT INTO %I VALUES(%L,%L)', NULL, 10, 'Hello'); + -- check positional placeholders + select format('%1$s %3$s', 1, 2, 3); + 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 + select format('Hello %s %1$s %s', 'World', 'Hello again'); + select format('Hello %s %s, %2$s %2$s', 'World', 'Hello again');
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers