blob: 8c9afccefc3d974f9d8ac153ac050982f9de889d [file]
/*-------------------------------------------------------------------------
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* try_convert.c
* Error-safe type cast function
*
* try_convert(source_value, default_value) casts source_value to the type of
* default_value. Whenever the cast fails because of the data being converted,
* default_value is returned instead of an error being raised.
*
* The conversion to use is looked up the same way the parser does it in
* coerce_type(), see src/backend/parser/parse_coerce.c.
*
* IDENTIFICATION
* contrib/try_convert/try_convert.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_type.h"
#include "funcapi.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_coerce.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(try_convert);
/*
* How a value has to be converted; a subset of the CoercionPathType values
* used by the parser.
*/
typedef enum ConversionType
{
CONVERSION_TYPE_FUNC,
CONVERSION_TYPE_RELABEL,
CONVERSION_TYPE_VIA_IO,
CONVERSION_TYPE_ARRAY,
CONVERSION_TYPE_NONE
} ConversionType;
static ConversionType find_conversion_way(Oid targetTypeId, Oid sourceTypeId,
Oid *funcId);
static ConversionType find_typmod_conversion_function(Oid typeId, Oid *funcId);
static void report_conversion_error(MemoryContext oldcontext, bool *is_failed);
static Datum convert_from_function(Datum value, int32 typmod, Oid funcId,
bool *is_failed);
static Datum convert_via_io(Datum value, Oid sourceTypeId, Oid targetTypeId,
bool *is_failed);
static int32 get_call_expr_argtypmod(Node *expr, int argnum);
static int32 get_fn_expr_argtypmod(FmgrInfo *flinfo, int argnum);
static Datum convert(Datum value, ConversionType conversion_type, Oid funcId,
Oid sourceTypeId, Oid targetTypeId, int32 targetTypMod,
bool *is_failed);
static Datum convert_type_typmod(Datum value, int32 sourceTypMod,
Oid targetTypeId, int32 targetTypMod,
bool *is_failed);
/*
* Determine how to convert sourceTypeId to targetTypeId, mirroring
* find_coercion_pathway() without the coercion context: try_convert() always
* performs an explicit cast.
*
* Returns CONVERSION_TYPE_NONE if no conversion is possible. *funcId is set
* to the conversion function when one is needed, to InvalidOid otherwise.
*/
static ConversionType
find_conversion_way(Oid targetTypeId, Oid sourceTypeId, Oid *funcId)
{
ConversionType result = CONVERSION_TYPE_NONE;
HeapTuple tuple;
*funcId = InvalidOid;
/*
* Both types have to be known. An invalid type OID means the caller could
* not resolve the argument type, and the lookups below -- TypeCategory()
* in particular -- do not accept one.
*/
if (!OidIsValid(sourceTypeId) || !OidIsValid(targetTypeId))
return CONVERSION_TYPE_NONE;
/* Perhaps the types are domains; if so, look at their base types */
sourceTypeId = getBaseType(sourceTypeId);
targetTypeId = getBaseType(targetTypeId);
/* Domains are always coercible to and from their base type */
if (sourceTypeId == targetTypeId)
return CONVERSION_TYPE_RELABEL;
/* Look in pg_cast */
tuple = SearchSysCache2(CASTSOURCETARGET,
ObjectIdGetDatum(sourceTypeId),
ObjectIdGetDatum(targetTypeId));
if (HeapTupleIsValid(tuple))
{
Form_pg_cast castForm = (Form_pg_cast) GETSTRUCT(tuple);
switch (castForm->castmethod)
{
case COERCION_METHOD_FUNCTION:
*funcId = castForm->castfunc;
result = CONVERSION_TYPE_FUNC;
break;
case COERCION_METHOD_INOUT:
result = CONVERSION_TYPE_VIA_IO;
break;
case COERCION_METHOD_BINARY:
result = CONVERSION_TYPE_RELABEL;
break;
default:
elog(ERROR, "unrecognized castmethod: %d",
(int) castForm->castmethod);
break;
}
ReleaseSysCache(tuple);
}
else
{
/*
* If there's no pg_cast entry, perhaps we are dealing with a pair of
* array types. If so, and if the element types have a suitable cast,
* report that we can coerce with an ArrayCoerceExpr.
*
* Note that the source type can be a domain over array, but not the
* target, because ArrayCoerceExpr won't check domain constraints.
*
* Hack: disallow coercions to oidvector and int2vector, which
* otherwise tend to capture coercions that should go to "real" array
* types. We want those types to be considered "real" arrays for many
* purposes, but not this one. (Also, ArrayCoerceExpr isn't
* guaranteed to produce an output that meets the restrictions of
* these datatypes, such as being 1-dimensional.)
*/
if (targetTypeId != OIDVECTOROID && targetTypeId != INT2VECTOROID)
{
Oid targetElem;
Oid sourceElem;
if ((targetElem = get_element_type(targetTypeId)) != InvalidOid &&
(sourceElem = get_base_element_type(sourceTypeId)) != InvalidOid)
{
ConversionType elempathtype;
Oid elemfuncid;
elempathtype = find_conversion_way(targetElem,
sourceElem,
&elemfuncid);
if (elempathtype != CONVERSION_TYPE_NONE &&
elempathtype != CONVERSION_TYPE_ARRAY)
{
*funcId = elemfuncid;
if (elempathtype == CONVERSION_TYPE_VIA_IO)
result = CONVERSION_TYPE_VIA_IO;
else
result = CONVERSION_TYPE_ARRAY;
}
}
}
/*
* If we still haven't found a possibility, consider automatic casting
* using I/O functions. We allow assignment casts to string types and
* explicit casts from string types to be handled this way. (The
* CoerceViaIO mechanism is a lot more general than that, but this is
* all we want to allow in the absence of a pg_cast entry.) It would
* probably be better to insist on explicit casts in both directions,
* but this is a compromise to preserve something of the pre-8.3
* behavior that many types had implicit (yipes!) casts to text.
*/
if (result == CONVERSION_TYPE_NONE)
{
if (TypeCategory(targetTypeId) == TYPCATEGORY_STRING)
result = CONVERSION_TYPE_VIA_IO;
else if (TypeCategory(sourceTypeId) == TYPCATEGORY_STRING)
result = CONVERSION_TYPE_VIA_IO;
}
}
return result;
}
/*
* Look up the length coercion function of a type, that is the cast from the
* type to itself, mirroring find_typmod_coercion_function().
*
* Returns CONVERSION_TYPE_NONE when the type has no length coercion function,
* in which case the value has to be left alone.
*/
static ConversionType
find_typmod_conversion_function(Oid typeId, Oid *funcId)
{
ConversionType result = CONVERSION_TYPE_NONE;
HeapTuple tuple;
*funcId = InvalidOid;
/* Look in pg_cast */
tuple = SearchSysCache2(CASTSOURCETARGET,
ObjectIdGetDatum(typeId),
ObjectIdGetDatum(typeId));
if (HeapTupleIsValid(tuple))
{
Form_pg_cast castForm = (Form_pg_cast) GETSTRUCT(tuple);
*funcId = castForm->castfunc;
ReleaseSysCache(tuple);
/*
* A binary-coercible self-cast carries no function, so there is
* nothing we could call to apply the typmod.
*/
if (OidIsValid(*funcId))
result = CONVERSION_TYPE_FUNC;
}
return result;
}
/*
* Common tail of the PG_CATCH() blocks below.
*
* The conversion functions called by this module report a failure the only way
* they can, by throwing an error, so the only way to keep going is to catch it.
* Ideally we would use the "soft" error handling infrastructure added by
* PostgreSQL 17 (commit ccff2d20ed) instead, which lets an input function
* report a conversion failure without throwing; that requires converting the
* datatype input functions first, so until then we trap the error here.
*
* Trapping an error outside of a subtransaction is only safe as long as the
* called function leaves no global state behind, which holds for the cast and
* type input/output functions we call. Errors that do not come from the data
* being converted must not be swallowed, so query cancellation and assertion
* failures are re-thrown, following what plpgsql does for "EXCEPTION WHEN
* others".
*/
static void
report_conversion_error(MemoryContext oldcontext, bool *is_failed)
{
int sqlerrcode = geterrcode();
if (sqlerrcode == ERRCODE_QUERY_CANCELED ||
sqlerrcode == ERRCODE_ASSERT_FAILURE)
PG_RE_THROW();
MemoryContextSwitchTo(oldcontext);
FlushErrorState();
*is_failed = true;
}
/*
* Convert a value by calling the cast function funcId.
*/
static Datum
convert_from_function(Datum value, int32 typmod, Oid funcId, bool *is_failed)
{
MemoryContext oldcontext = CurrentMemoryContext;
volatile Datum res = (Datum) 0;
PG_TRY();
{
/*
* Cast functions take either one argument or three, the extra ones
* being the target typmod and the explicit-cast flag. Passing three
* arguments to a one-argument function is harmless, the callee simply
* ignores them.
*/
res = OidFunctionCall3(funcId,
value,
Int32GetDatum(typmod),
BoolGetDatum(true));
}
PG_CATCH();
{
report_conversion_error(oldcontext, is_failed);
}
PG_END_TRY();
return res;
}
/*
* Convert a value by running it through the output function of the source type
* and the input function of the target type.
*
* The typmod is not applied here, convert_type_typmod() takes care of it.
*/
static Datum
convert_via_io(Datum value, Oid sourceTypeId, Oid targetTypeId,
bool *is_failed)
{
FmgrInfo outfunc;
Oid infuncId = InvalidOid;
Oid outfuncId = InvalidOid;
Oid intypioparam = InvalidOid;
bool outtypisvarlena = false;
MemoryContext oldcontext = CurrentMemoryContext;
volatile Datum res = (Datum) 0;
/* Perhaps the types are domains; if so, look at their base types */
sourceTypeId = getBaseType(sourceTypeId);
targetTypeId = getBaseType(targetTypeId);
getTypeOutputInfo(sourceTypeId, &outfuncId, &outtypisvarlena);
fmgr_info(outfuncId, &outfunc);
getTypeInputInfo(targetTypeId, &infuncId, &intypioparam);
PG_TRY();
{
char *string;
/* the caller has already rejected a NULL input value */
string = OutputFunctionCall(&outfunc, value);
res = OidFunctionCall3(infuncId,
CStringGetDatum(string),
ObjectIdGetDatum(intypioparam),
Int32GetDatum(-1));
pfree(string);
}
PG_CATCH();
{
report_conversion_error(oldcontext, is_failed);
}
PG_END_TRY();
return res;
}
/*
* Get the actual typmod of a specific function argument (counting from 0),
* but working from the calling expression tree instead of FmgrInfo.
*
* Returns -1 if information is not available.
*/
static int32
get_call_expr_argtypmod(Node *expr, int argnum)
{
List *args;
if (expr == NULL)
return -1;
if (IsA(expr, FuncExpr))
args = ((FuncExpr *) expr)->args;
else if (IsA(expr, OpExpr))
args = ((OpExpr *) expr)->args;
else if (IsA(expr, DistinctExpr))
args = ((DistinctExpr *) expr)->args;
else if (IsA(expr, ScalarArrayOpExpr))
args = ((ScalarArrayOpExpr *) expr)->args;
else if (IsA(expr, ArrayCoerceExpr))
args = list_make1(((ArrayCoerceExpr *) expr)->arg);
else if (IsA(expr, NullIfExpr))
args = ((NullIfExpr *) expr)->args;
else if (IsA(expr, WindowFunc))
args = ((WindowFunc *) expr)->args;
else
return -1;
if (argnum < 0 || argnum >= list_length(args))
return -1;
return exprTypmod((Node *) list_nth(args, argnum));
}
/*
* Get the actual typmod of a specific function argument (counting from 0).
*
* Returns -1 if information is not available.
*/
static int32
get_fn_expr_argtypmod(FmgrInfo *flinfo, int argnum)
{
/*
* can't return anything useful if we have no FmgrInfo or if its fn_expr
* node has not been initialized
*/
if (!flinfo || !flinfo->fn_expr)
return -1;
return get_call_expr_argtypmod(flinfo->fn_expr, argnum);
}
/*
* Apply the conversion found by find_conversion_way() or
* find_typmod_conversion_function().
*
* A conversion that cannot be performed at all is a query error and is
* reported as such; only failures caused by the data being converted are
* reported through *is_failed.
*/
static Datum
convert(Datum value, ConversionType conversion_type, Oid funcId,
Oid sourceTypeId, Oid targetTypeId, int32 targetTypMod,
bool *is_failed)
{
switch (conversion_type)
{
case CONVERSION_TYPE_RELABEL:
return value;
case CONVERSION_TYPE_FUNC:
return convert_from_function(value, targetTypMod, funcId,
is_failed);
case CONVERSION_TYPE_VIA_IO:
return convert_via_io(value, sourceTypeId, targetTypeId,
is_failed);
case CONVERSION_TYPE_ARRAY:
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("try_convert() does not support casts between array types"),
errdetail("Cannot cast type %s to %s.",
format_type_be(sourceTypeId),
format_type_be(targetTypeId))));
break;
case CONVERSION_TYPE_NONE:
ereport(ERROR,
(errcode(ERRCODE_CANNOT_COERCE),
errmsg("cannot cast type %s to %s",
format_type_be(sourceTypeId),
format_type_be(targetTypeId))));
break;
}
elog(ERROR, "unrecognized conversion method: %d", (int) conversion_type);
return (Datum) 0; /* keep compiler quiet */
}
/*
* Coerce a value of targetTypeId to targetTypMod.
*/
static Datum
convert_type_typmod(Datum value, int32 sourceTypMod, Oid targetTypeId,
int32 targetTypMod, bool *is_failed)
{
ConversionType conversion_type;
Oid funcId;
if (targetTypMod < 0 || targetTypMod == sourceTypMod)
return value;
conversion_type = find_typmod_conversion_function(targetTypeId, &funcId);
/*
* If the target type has no length coercion function, just leave the value
* alone, the same way coerce_type_typmod() does.
*/
if (conversion_type == CONVERSION_TYPE_NONE)
return value;
return convert(value, conversion_type, funcId, targetTypeId, targetTypeId,
targetTypMod, is_failed);
}
/*
* try_convert(source_value, default_value) -> converted value or default_value
*
* The target type is taken from the second argument, which is also the value
* returned when the conversion fails.
*/
Datum
try_convert(PG_FUNCTION_ARGS)
{
Oid sourceTypeId;
int32 sourceTypMod;
Oid targetTypeId;
int32 targetTypMod;
Oid baseTypeId;
int32 baseTypMod;
Oid funcId;
ConversionType conversion_type;
Datum value;
Datum res;
int32 resTypMod;
bool is_failed = false;
/* A NULL input converts to NULL, whatever the default value is */
if (PG_ARGISNULL(0))
PG_RETURN_NULL();
sourceTypeId = get_fn_expr_argtype(fcinfo->flinfo, 0);
sourceTypMod = get_fn_expr_argtypmod(fcinfo->flinfo, 0);
targetTypeId = get_fn_expr_argtype(fcinfo->flinfo, 1);
targetTypMod = get_fn_expr_argtypmod(fcinfo->flinfo, 1);
if (!OidIsValid(sourceTypeId) || !OidIsValid(targetTypeId))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("could not determine the argument types of try_convert()")));
baseTypMod = targetTypMod;
baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypMod);
if (targetTypeId != baseTypeId)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("try_convert() does not support casts to domain types"),
errdetail("Cannot cast type %s to %s.",
format_type_be(sourceTypeId),
format_type_be(targetTypeId))));
value = PG_GETARG_DATUM(0);
conversion_type = find_conversion_way(targetTypeId, sourceTypeId, &funcId);
if (conversion_type == CONVERSION_TYPE_RELABEL)
{
res = value;
resTypMod = sourceTypMod;
}
else
{
res = convert(value, conversion_type, funcId, sourceTypeId, baseTypeId,
baseTypMod, &is_failed);
resTypMod = -1;
}
if (!is_failed)
res = convert_type_typmod(res, resTypMod, targetTypeId, targetTypMod,
&is_failed);
if (is_failed)
{
/* the value could not be converted, fall back to the default one */
fcinfo->isnull = PG_ARGISNULL(1);
return PG_GETARG_DATUM(1);
}
return res;
}