diff options
Diffstat (limited to 'contrib/jsonb_plpython/jsonb_plpython.c')
-rw-r--r-- | contrib/jsonb_plpython/jsonb_plpython.c | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c new file mode 100644 index 0000000..03bbfa8 --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -0,0 +1,504 @@ +#include "postgres.h" + +#include "plpy_elog.h" +#include "plpy_typeio.h" +#include "plpython.h" +#include "utils/fmgrprotos.h" +#include "utils/jsonb.h" +#include "utils/numeric.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +/* for PLyObject_AsString in plpy_typeio.c */ +typedef char *(*PLyObject_AsString_t) (PyObject *plrv); +static PLyObject_AsString_t PLyObject_AsString_p; + +typedef void (*PLy_elog_impl_t) (int elevel, const char *fmt,...); +static PLy_elog_impl_t PLy_elog_impl_p; + +/* + * decimal_constructor is a function from python library and used + * for transforming strings into python decimal type + */ +static PyObject *decimal_constructor; + +static PyObject *PLyObject_FromJsonbContainer(JsonbContainer *jsonb); +static JsonbValue *PLyObject_ToJsonbValue(PyObject *obj, + JsonbParseState **jsonb_state, bool is_elem); + +typedef PyObject *(*PLyUnicode_FromStringAndSize_t) + (const char *s, Py_ssize_t size); +static PLyUnicode_FromStringAndSize_t PLyUnicode_FromStringAndSize_p; + +/* + * Module initialize function: fetch function pointers for cross-module calls. + */ +void +_PG_init(void) +{ + /* Asserts verify that typedefs above match original declarations */ + AssertVariableIsOfType(&PLyObject_AsString, PLyObject_AsString_t); + PLyObject_AsString_p = (PLyObject_AsString_t) + load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyObject_AsString", + true, NULL); + AssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); + PLyUnicode_FromStringAndSize_p = (PLyUnicode_FromStringAndSize_t) + load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyUnicode_FromStringAndSize", + true, NULL); + AssertVariableIsOfType(&PLy_elog_impl, PLy_elog_impl_t); + PLy_elog_impl_p = (PLy_elog_impl_t) + load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLy_elog_impl", + true, NULL); +} + +/* These defines must be after the _PG_init */ +#define PLyObject_AsString (PLyObject_AsString_p) +#define PLyUnicode_FromStringAndSize (PLyUnicode_FromStringAndSize_p) +#undef PLy_elog +#define PLy_elog (PLy_elog_impl_p) + +/* + * PLyUnicode_FromJsonbValue + * + * Transform string JsonbValue to Python string. + */ +static PyObject * +PLyUnicode_FromJsonbValue(JsonbValue *jbv) +{ + Assert(jbv->type == jbvString); + + return PLyUnicode_FromStringAndSize(jbv->val.string.val, jbv->val.string.len); +} + +/* + * PLyUnicode_ToJsonbValue + * + * Transform Python string to JsonbValue. + */ +static void +PLyUnicode_ToJsonbValue(PyObject *obj, JsonbValue *jbvElem) +{ + jbvElem->type = jbvString; + jbvElem->val.string.val = PLyObject_AsString(obj); + jbvElem->val.string.len = strlen(jbvElem->val.string.val); +} + +/* + * PLyObject_FromJsonbValue + * + * Transform JsonbValue to PyObject. + */ +static PyObject * +PLyObject_FromJsonbValue(JsonbValue *jsonbValue) +{ + switch (jsonbValue->type) + { + case jbvNull: + Py_RETURN_NONE; + + case jbvBinary: + return PLyObject_FromJsonbContainer(jsonbValue->val.binary.data); + + case jbvNumeric: + { + Datum num; + char *str; + + num = NumericGetDatum(jsonbValue->val.numeric); + str = DatumGetCString(DirectFunctionCall1(numeric_out, num)); + + return PyObject_CallFunction(decimal_constructor, "s", str); + } + + case jbvString: + return PLyUnicode_FromJsonbValue(jsonbValue); + + case jbvBool: + if (jsonbValue->val.boolean) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; + + default: + elog(ERROR, "unexpected jsonb value type: %d", jsonbValue->type); + return NULL; + } +} + +/* + * PLyObject_FromJsonbContainer + * + * Transform JsonbContainer to PyObject. + */ +static PyObject * +PLyObject_FromJsonbContainer(JsonbContainer *jsonb) +{ + JsonbIteratorToken r; + JsonbValue v; + JsonbIterator *it; + PyObject *result; + + it = JsonbIteratorInit(jsonb); + r = JsonbIteratorNext(&it, &v, true); + + switch (r) + { + case WJB_BEGIN_ARRAY: + if (v.val.array.rawScalar) + { + JsonbValue tmp; + + if ((r = JsonbIteratorNext(&it, &v, true)) != WJB_ELEM || + (r = JsonbIteratorNext(&it, &tmp, true)) != WJB_END_ARRAY || + (r = JsonbIteratorNext(&it, &tmp, true)) != WJB_DONE) + elog(ERROR, "unexpected jsonb token: %d", r); + + result = PLyObject_FromJsonbValue(&v); + } + else + { + PyObject *volatile elem = NULL; + + result = PyList_New(0); + if (!result) + return NULL; + + PG_TRY(); + { + while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE) + { + if (r != WJB_ELEM) + continue; + + elem = PLyObject_FromJsonbValue(&v); + + PyList_Append(result, elem); + Py_XDECREF(elem); + elem = NULL; + } + } + PG_CATCH(); + { + Py_XDECREF(elem); + Py_XDECREF(result); + PG_RE_THROW(); + } + PG_END_TRY(); + } + break; + + case WJB_BEGIN_OBJECT: + { + PyObject *volatile result_v = PyDict_New(); + PyObject *volatile key = NULL; + PyObject *volatile val = NULL; + + if (!result_v) + return NULL; + + PG_TRY(); + { + while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE) + { + if (r != WJB_KEY) + continue; + + key = PLyUnicode_FromJsonbValue(&v); + if (!key) + { + Py_XDECREF(result_v); + result_v = NULL; + break; + } + + if ((r = JsonbIteratorNext(&it, &v, true)) != WJB_VALUE) + elog(ERROR, "unexpected jsonb token: %d", r); + + val = PLyObject_FromJsonbValue(&v); + if (!val) + { + Py_XDECREF(key); + key = NULL; + Py_XDECREF(result_v); + result_v = NULL; + break; + } + + PyDict_SetItem(result_v, key, val); + + Py_XDECREF(key); + key = NULL; + Py_XDECREF(val); + val = NULL; + } + } + PG_CATCH(); + { + Py_XDECREF(result_v); + Py_XDECREF(key); + Py_XDECREF(val); + PG_RE_THROW(); + } + PG_END_TRY(); + + result = result_v; + } + break; + + default: + elog(ERROR, "unexpected jsonb token: %d", r); + return NULL; + } + + return result; +} + +/* + * PLyMapping_ToJsonbValue + * + * Transform Python dict to JsonbValue. + */ +static JsonbValue * +PLyMapping_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) +{ + Py_ssize_t pcount; + PyObject *volatile items; + JsonbValue *volatile out; + + pcount = PyMapping_Size(obj); + items = PyMapping_Items(obj); + + PG_TRY(); + { + Py_ssize_t i; + + pushJsonbValue(jsonb_state, WJB_BEGIN_OBJECT, NULL); + + for (i = 0; i < pcount; i++) + { + JsonbValue jbvKey; + PyObject *item = PyList_GetItem(items, i); + PyObject *key = PyTuple_GetItem(item, 0); + PyObject *value = PyTuple_GetItem(item, 1); + + /* Python dictionary can have None as key */ + if (key == Py_None) + { + jbvKey.type = jbvString; + jbvKey.val.string.len = 0; + jbvKey.val.string.val = ""; + } + else + { + /* All others types of keys we serialize to string */ + PLyUnicode_ToJsonbValue(key, &jbvKey); + } + + (void) pushJsonbValue(jsonb_state, WJB_KEY, &jbvKey); + (void) PLyObject_ToJsonbValue(value, jsonb_state, false); + } + + out = pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL); + } + PG_FINALLY(); + { + Py_DECREF(items); + } + PG_END_TRY(); + + return out; +} + +/* + * PLySequence_ToJsonbValue + * + * Transform python list to JsonbValue. Expects transformed PyObject and + * a state required for jsonb construction. + */ +static JsonbValue * +PLySequence_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) +{ + Py_ssize_t i; + Py_ssize_t pcount; + PyObject *volatile value = NULL; + + pcount = PySequence_Size(obj); + + pushJsonbValue(jsonb_state, WJB_BEGIN_ARRAY, NULL); + + PG_TRY(); + { + for (i = 0; i < pcount; i++) + { + value = PySequence_GetItem(obj, i); + Assert(value); + + (void) PLyObject_ToJsonbValue(value, jsonb_state, true); + Py_XDECREF(value); + value = NULL; + } + } + PG_CATCH(); + { + Py_XDECREF(value); + PG_RE_THROW(); + } + PG_END_TRY(); + + return pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL); +} + +/* + * PLyNumber_ToJsonbValue(PyObject *obj) + * + * Transform python number to JsonbValue. + */ +static JsonbValue * +PLyNumber_ToJsonbValue(PyObject *obj, JsonbValue *jbvNum) +{ + Numeric num; + char *str = PLyObject_AsString(obj); + + PG_TRY(); + { + Datum numd; + + numd = DirectFunctionCall3(numeric_in, + CStringGetDatum(str), + ObjectIdGetDatum(InvalidOid), + Int32GetDatum(-1)); + num = DatumGetNumeric(numd); + } + PG_CATCH(); + { + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("could not convert value \"%s\" to jsonb", str))); + } + PG_END_TRY(); + + pfree(str); + + /* + * jsonb doesn't allow NaN or infinity (per JSON specification), so we + * have to reject those here explicitly. + */ + if (numeric_is_nan(num)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("cannot convert NaN to jsonb"))); + if (numeric_is_inf(num)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("cannot convert infinity to jsonb"))); + + jbvNum->type = jbvNumeric; + jbvNum->val.numeric = num; + + return jbvNum; +} + +/* + * PLyObject_ToJsonbValue(PyObject *obj) + * + * Transform python object to JsonbValue. + */ +static JsonbValue * +PLyObject_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state, bool is_elem) +{ + JsonbValue *out; + + if (!PyUnicode_Check(obj)) + { + if (PySequence_Check(obj)) + return PLySequence_ToJsonbValue(obj, jsonb_state); + else if (PyMapping_Check(obj)) + return PLyMapping_ToJsonbValue(obj, jsonb_state); + } + + out = palloc(sizeof(JsonbValue)); + + if (obj == Py_None) + out->type = jbvNull; + else if (PyUnicode_Check(obj)) + PLyUnicode_ToJsonbValue(obj, out); + + /* + * PyNumber_Check() returns true for booleans, so boolean check should + * come first. + */ + else if (PyBool_Check(obj)) + { + out->type = jbvBool; + out->val.boolean = (obj == Py_True); + } + else if (PyNumber_Check(obj)) + out = PLyNumber_ToJsonbValue(obj, out); + else + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Python type \"%s\" cannot be transformed to jsonb", + PLyObject_AsString((PyObject *) obj->ob_type)))); + + /* Push result into 'jsonb_state' unless it is raw scalar value. */ + return (*jsonb_state ? + pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, out) : + out); +} + +/* + * plpython_to_jsonb + * + * Transform python object to Jsonb datum + */ +PG_FUNCTION_INFO_V1(plpython_to_jsonb); +Datum +plpython_to_jsonb(PG_FUNCTION_ARGS) +{ + PyObject *obj; + JsonbValue *out; + JsonbParseState *jsonb_state = NULL; + + obj = (PyObject *) PG_GETARG_POINTER(0); + out = PLyObject_ToJsonbValue(obj, &jsonb_state, true); + PG_RETURN_POINTER(JsonbValueToJsonb(out)); +} + +/* + * jsonb_to_plpython + * + * Transform Jsonb datum to PyObject and return it as internal. + */ +PG_FUNCTION_INFO_V1(jsonb_to_plpython); +Datum +jsonb_to_plpython(PG_FUNCTION_ARGS) +{ + PyObject *result; + Jsonb *in = PG_GETARG_JSONB_P(0); + + /* + * Initialize pointer to Decimal constructor. First we try "cdecimal", C + * version of decimal library. In case of failure we use slower "decimal" + * module. + */ + if (!decimal_constructor) + { + PyObject *decimal_module = PyImport_ImportModule("cdecimal"); + + if (!decimal_module) + { + PyErr_Clear(); + decimal_module = PyImport_ImportModule("decimal"); + } + Assert(decimal_module); + decimal_constructor = PyObject_GetAttrString(decimal_module, "Decimal"); + } + + result = PLyObject_FromJsonbContainer(&in->root); + if (!result) + PLy_elog(ERROR, "transformation from jsonb to Python failed"); + + return PointerGetDatum(result); +} |