LCOV - code coverage report
Current view: top level - contrib/jsonb_plpython - jsonb_plpython.c (source / functions) Hit Total Coverage
Test: PostgreSQL 19devel Lines: 163 197 82.7 %
Date: 2026-02-07 10:18:48 Functions: 14 14 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : #include "postgres.h"
       2             : 
       3             : #include "plpy_elog.h"
       4             : #include "plpy_typeio.h"
       5             : #include "plpy_util.h"
       6             : #include "utils/fmgrprotos.h"
       7             : #include "utils/jsonb.h"
       8             : #include "utils/numeric.h"
       9             : 
      10           2 : PG_MODULE_MAGIC_EXT(
      11             :                     .name = "jsonb_plpython",
      12             :                     .version = PG_VERSION
      13             : );
      14             : 
      15             : /* for PLyObject_AsString in plpy_typeio.c */
      16             : typedef char *(*PLyObject_AsString_t) (PyObject *plrv);
      17             : static PLyObject_AsString_t PLyObject_AsString_p;
      18             : 
      19             : typedef void (*PLy_elog_impl_t) (int elevel, const char *fmt,...);
      20             : static PLy_elog_impl_t PLy_elog_impl_p;
      21             : 
      22             : /*
      23             :  * decimal_constructor is a function from python library and used
      24             :  * for transforming strings into python decimal type
      25             :  */
      26             : static PyObject *decimal_constructor;
      27             : 
      28             : static PyObject *PLyObject_FromJsonbContainer(JsonbContainer *jsonb);
      29             : static void PLyObject_ToJsonbValue(PyObject *obj,
      30             :                                    JsonbInState *jsonb_state, bool is_elem);
      31             : 
      32             : typedef PyObject *(*PLyUnicode_FromStringAndSize_t)
      33             :             (const char *s, Py_ssize_t size);
      34             : static PLyUnicode_FromStringAndSize_t PLyUnicode_FromStringAndSize_p;
      35             : 
      36             : /* Static asserts verify that typedefs above match original declarations */
      37             : StaticAssertVariableIsOfType(&PLyObject_AsString, PLyObject_AsString_t);
      38             : StaticAssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t);
      39             : StaticAssertVariableIsOfType(&PLy_elog_impl, PLy_elog_impl_t);
      40             : 
      41             : 
      42             : /*
      43             :  * Module initialize function: fetch function pointers for cross-module calls.
      44             :  */
      45             : void
      46           2 : _PG_init(void)
      47             : {
      48           2 :     PLyObject_AsString_p = (PLyObject_AsString_t)
      49           2 :         load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyObject_AsString",
      50             :                                true, NULL);
      51           2 :     PLyUnicode_FromStringAndSize_p = (PLyUnicode_FromStringAndSize_t)
      52           2 :         load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyUnicode_FromStringAndSize",
      53             :                                true, NULL);
      54           2 :     PLy_elog_impl_p = (PLy_elog_impl_t)
      55           2 :         load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLy_elog_impl",
      56             :                                true, NULL);
      57           2 : }
      58             : 
      59             : /* These defines must be after the _PG_init */
      60             : #define PLyObject_AsString (PLyObject_AsString_p)
      61             : #define PLyUnicode_FromStringAndSize (PLyUnicode_FromStringAndSize_p)
      62             : #undef PLy_elog
      63             : #define PLy_elog (PLy_elog_impl_p)
      64             : 
      65             : /*
      66             :  * PLyUnicode_FromJsonbValue
      67             :  *
      68             :  * Transform string JsonbValue to Python string.
      69             :  */
      70             : static PyObject *
      71          42 : PLyUnicode_FromJsonbValue(JsonbValue *jbv)
      72             : {
      73             :     Assert(jbv->type == jbvString);
      74             : 
      75          42 :     return PLyUnicode_FromStringAndSize(jbv->val.string.val, jbv->val.string.len);
      76             : }
      77             : 
      78             : /*
      79             :  * PLyUnicode_ToJsonbValue
      80             :  *
      81             :  * Transform Python string to JsonbValue.
      82             :  */
      83             : static void
      84          26 : PLyUnicode_ToJsonbValue(PyObject *obj, JsonbValue *jbvElem)
      85             : {
      86          26 :     jbvElem->type = jbvString;
      87          26 :     jbvElem->val.string.val = PLyObject_AsString(obj);
      88          26 :     jbvElem->val.string.len = strlen(jbvElem->val.string.val);
      89          26 : }
      90             : 
      91             : /*
      92             :  * PLyObject_FromJsonbValue
      93             :  *
      94             :  * Transform JsonbValue to PyObject.
      95             :  */
      96             : static PyObject *
      97          80 : PLyObject_FromJsonbValue(JsonbValue *jsonbValue)
      98             : {
      99          80 :     switch (jsonbValue->type)
     100             :     {
     101          10 :         case jbvNull:
     102          10 :             Py_RETURN_NONE;
     103             : 
     104           8 :         case jbvBinary:
     105           8 :             return PLyObject_FromJsonbContainer(jsonbValue->val.binary.data);
     106             : 
     107          36 :         case jbvNumeric:
     108             :             {
     109             :                 Datum       num;
     110             :                 char       *str;
     111             : 
     112          36 :                 num = NumericGetDatum(jsonbValue->val.numeric);
     113          36 :                 str = DatumGetCString(DirectFunctionCall1(numeric_out, num));
     114             : 
     115          36 :                 return PyObject_CallFunction(decimal_constructor, "s", str);
     116             :             }
     117             : 
     118          16 :         case jbvString:
     119          16 :             return PLyUnicode_FromJsonbValue(jsonbValue);
     120             : 
     121          10 :         case jbvBool:
     122          10 :             if (jsonbValue->val.boolean)
     123          10 :                 Py_RETURN_TRUE;
     124             :             else
     125           0 :                 Py_RETURN_FALSE;
     126             : 
     127           0 :         default:
     128           0 :             elog(ERROR, "unexpected jsonb value type: %d", jsonbValue->type);
     129             :             return NULL;
     130             :     }
     131             : }
     132             : 
     133             : /*
     134             :  * PLyObject_FromJsonbContainer
     135             :  *
     136             :  * Transform JsonbContainer to PyObject.
     137             :  */
     138             : static PyObject *
     139          60 : PLyObject_FromJsonbContainer(JsonbContainer *jsonb)
     140             : {
     141             :     JsonbIteratorToken r;
     142             :     JsonbValue  v;
     143             :     JsonbIterator *it;
     144             :     PyObject   *result;
     145             : 
     146          60 :     it = JsonbIteratorInit(jsonb);
     147          60 :     r = JsonbIteratorNext(&it, &v, true);
     148             : 
     149          60 :     switch (r)
     150             :     {
     151          40 :         case WJB_BEGIN_ARRAY:
     152          40 :             if (v.val.array.rawScalar)
     153             :             {
     154             :                 JsonbValue  tmp;
     155             : 
     156          18 :                 if ((r = JsonbIteratorNext(&it, &v, true)) != WJB_ELEM ||
     157          18 :                     (r = JsonbIteratorNext(&it, &tmp, true)) != WJB_END_ARRAY ||
     158          18 :                     (r = JsonbIteratorNext(&it, &tmp, true)) != WJB_DONE)
     159           0 :                     elog(ERROR, "unexpected jsonb token: %d", r);
     160             : 
     161          18 :                 result = PLyObject_FromJsonbValue(&v);
     162             :             }
     163             :             else
     164             :             {
     165          22 :                 PyObject   *volatile elem = NULL;
     166             : 
     167          22 :                 result = PyList_New(0);
     168          22 :                 if (!result)
     169           0 :                     return NULL;
     170             : 
     171          22 :                 PG_TRY();
     172             :                 {
     173          80 :                     while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE)
     174             :                     {
     175          58 :                         if (r != WJB_ELEM)
     176          22 :                             continue;
     177             : 
     178          36 :                         elem = PLyObject_FromJsonbValue(&v);
     179             : 
     180          36 :                         PyList_Append(result, elem);
     181          36 :                         Py_XDECREF(elem);
     182          36 :                         elem = NULL;
     183             :                     }
     184             :                 }
     185           0 :                 PG_CATCH();
     186             :                 {
     187           0 :                     Py_XDECREF(elem);
     188           0 :                     Py_XDECREF(result);
     189           0 :                     PG_RE_THROW();
     190             :                 }
     191          22 :                 PG_END_TRY();
     192             :             }
     193          40 :             break;
     194             : 
     195          20 :         case WJB_BEGIN_OBJECT:
     196             :             {
     197          20 :                 PyObject   *volatile result_v = PyDict_New();
     198          20 :                 PyObject   *volatile key = NULL;
     199          20 :                 PyObject   *volatile val = NULL;
     200             : 
     201          20 :                 if (!result_v)
     202           0 :                     return NULL;
     203             : 
     204          20 :                 PG_TRY();
     205             :                 {
     206          66 :                     while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE)
     207             :                     {
     208          46 :                         if (r != WJB_KEY)
     209          20 :                             continue;
     210             : 
     211          26 :                         key = PLyUnicode_FromJsonbValue(&v);
     212          26 :                         if (!key)
     213             :                         {
     214           0 :                             Py_XDECREF(result_v);
     215           0 :                             result_v = NULL;
     216           0 :                             break;
     217             :                         }
     218             : 
     219          26 :                         if ((r = JsonbIteratorNext(&it, &v, true)) != WJB_VALUE)
     220           0 :                             elog(ERROR, "unexpected jsonb token: %d", r);
     221             : 
     222          26 :                         val = PLyObject_FromJsonbValue(&v);
     223          26 :                         if (!val)
     224             :                         {
     225           0 :                             Py_XDECREF(key);
     226           0 :                             key = NULL;
     227           0 :                             Py_XDECREF(result_v);
     228           0 :                             result_v = NULL;
     229           0 :                             break;
     230             :                         }
     231             : 
     232          26 :                         PyDict_SetItem(result_v, key, val);
     233             : 
     234          26 :                         Py_XDECREF(key);
     235          26 :                         key = NULL;
     236          26 :                         Py_XDECREF(val);
     237          26 :                         val = NULL;
     238             :                     }
     239             :                 }
     240           0 :                 PG_CATCH();
     241             :                 {
     242           0 :                     Py_XDECREF(result_v);
     243           0 :                     Py_XDECREF(key);
     244           0 :                     Py_XDECREF(val);
     245           0 :                     PG_RE_THROW();
     246             :                 }
     247          20 :                 PG_END_TRY();
     248             : 
     249          20 :                 result = result_v;
     250             :             }
     251          20 :             break;
     252             : 
     253           0 :         default:
     254           0 :             elog(ERROR, "unexpected jsonb token: %d", r);
     255             :             return NULL;
     256             :     }
     257             : 
     258          60 :     return result;
     259             : }
     260             : 
     261             : /*
     262             :  * PLyMapping_ToJsonbValue
     263             :  *
     264             :  * Transform Python dict to JsonbValue.
     265             :  */
     266             : static void
     267          10 : PLyMapping_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state)
     268             : {
     269             :     Py_ssize_t  pcount;
     270             :     PyObject   *volatile items;
     271             : 
     272          10 :     pcount = PyMapping_Size(obj);
     273          10 :     items = PyMapping_Items(obj);
     274             : 
     275          10 :     PG_TRY();
     276             :     {
     277             :         Py_ssize_t  i;
     278             : 
     279          10 :         pushJsonbValue(jsonb_state, WJB_BEGIN_OBJECT, NULL);
     280             : 
     281          24 :         for (i = 0; i < pcount; i++)
     282             :         {
     283             :             JsonbValue  jbvKey;
     284          14 :             PyObject   *item = PyList_GetItem(items, i);
     285          14 :             PyObject   *key = PyTuple_GetItem(item, 0);
     286          14 :             PyObject   *value = PyTuple_GetItem(item, 1);
     287             : 
     288             :             /* Python dictionary can have None as key */
     289          14 :             if (key == Py_None)
     290             :             {
     291           2 :                 jbvKey.type = jbvString;
     292           2 :                 jbvKey.val.string.len = 0;
     293           2 :                 jbvKey.val.string.val = "";
     294             :             }
     295             :             else
     296             :             {
     297             :                 /* All others types of keys we serialize to string */
     298          12 :                 PLyUnicode_ToJsonbValue(key, &jbvKey);
     299             :             }
     300             : 
     301          14 :             pushJsonbValue(jsonb_state, WJB_KEY, &jbvKey);
     302          14 :             PLyObject_ToJsonbValue(value, jsonb_state, false);
     303             :         }
     304             : 
     305          10 :         pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL);
     306             :     }
     307           0 :     PG_FINALLY();
     308             :     {
     309          10 :         Py_DECREF(items);
     310             :     }
     311          10 :     PG_END_TRY();
     312          10 : }
     313             : 
     314             : /*
     315             :  * PLySequence_ToJsonbValue
     316             :  *
     317             :  * Transform python list to JsonbValue. Expects transformed PyObject and
     318             :  * a state required for jsonb construction.
     319             :  */
     320             : static void
     321          20 : PLySequence_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state)
     322             : {
     323             :     Py_ssize_t  i;
     324             :     Py_ssize_t  pcount;
     325          20 :     PyObject   *volatile value = NULL;
     326             : 
     327          20 :     pcount = PySequence_Size(obj);
     328             : 
     329          20 :     pushJsonbValue(jsonb_state, WJB_BEGIN_ARRAY, NULL);
     330             : 
     331          20 :     PG_TRY();
     332             :     {
     333          56 :         for (i = 0; i < pcount; i++)
     334             :         {
     335          36 :             value = PySequence_GetItem(obj, i);
     336             :             Assert(value);
     337             : 
     338          36 :             PLyObject_ToJsonbValue(value, jsonb_state, true);
     339          36 :             Py_XDECREF(value);
     340          36 :             value = NULL;
     341             :         }
     342             :     }
     343           0 :     PG_CATCH();
     344             :     {
     345           0 :         Py_XDECREF(value);
     346           0 :         PG_RE_THROW();
     347             :     }
     348          20 :     PG_END_TRY();
     349             : 
     350          20 :     pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL);
     351          20 : }
     352             : 
     353             : /*
     354             :  * PLyNumber_ToJsonbValue(PyObject *obj)
     355             :  *
     356             :  * Transform python number to JsonbValue.
     357             :  */
     358             : static JsonbValue *
     359          32 : PLyNumber_ToJsonbValue(PyObject *obj, JsonbValue *jbvNum)
     360             : {
     361             :     Numeric     num;
     362          32 :     char       *str = PLyObject_AsString(obj);
     363             : 
     364          32 :     PG_TRY();
     365             :     {
     366             :         Datum       numd;
     367             : 
     368          32 :         numd = DirectFunctionCall3(numeric_in,
     369             :                                    CStringGetDatum(str),
     370             :                                    ObjectIdGetDatum(InvalidOid),
     371             :                                    Int32GetDatum(-1));
     372          30 :         num = DatumGetNumeric(numd);
     373             :     }
     374           2 :     PG_CATCH();
     375             :     {
     376           2 :         ereport(ERROR,
     377             :                 (errcode(ERRCODE_DATATYPE_MISMATCH),
     378             :                  errmsg("could not convert value \"%s\" to jsonb", str)));
     379             :     }
     380          30 :     PG_END_TRY();
     381             : 
     382          30 :     pfree(str);
     383             : 
     384             :     /*
     385             :      * jsonb doesn't allow NaN or infinity (per JSON specification), so we
     386             :      * have to reject those here explicitly.
     387             :      */
     388          30 :     if (numeric_is_nan(num))
     389           0 :         ereport(ERROR,
     390             :                 (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
     391             :                  errmsg("cannot convert NaN to jsonb")));
     392          30 :     if (numeric_is_inf(num))
     393           0 :         ereport(ERROR,
     394             :                 (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
     395             :                  errmsg("cannot convert infinity to jsonb")));
     396             : 
     397          30 :     jbvNum->type = jbvNumeric;
     398          30 :     jbvNum->val.numeric = num;
     399             : 
     400          30 :     return jbvNum;
     401             : }
     402             : 
     403             : /*
     404             :  * PLyObject_ToJsonbValue(PyObject *obj)
     405             :  *
     406             :  * Transform python object to JsonbValue.
     407             :  */
     408             : static void
     409          94 : PLyObject_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state, bool is_elem)
     410             : {
     411             :     JsonbValue *out;
     412             : 
     413          94 :     if (!PyUnicode_Check(obj))
     414             :     {
     415          80 :         if (PySequence_Check(obj))
     416             :         {
     417          20 :             PLySequence_ToJsonbValue(obj, jsonb_state);
     418          20 :             return;
     419             :         }
     420          60 :         else if (PyMapping_Check(obj))
     421             :         {
     422          10 :             PLyMapping_ToJsonbValue(obj, jsonb_state);
     423          10 :             return;
     424             :         }
     425             :     }
     426             : 
     427          64 :     out = palloc_object(JsonbValue);
     428             : 
     429          64 :     if (obj == Py_None)
     430           8 :         out->type = jbvNull;
     431          56 :     else if (PyUnicode_Check(obj))
     432          14 :         PLyUnicode_ToJsonbValue(obj, out);
     433             : 
     434             :     /*
     435             :      * PyNumber_Check() returns true for booleans, so boolean check should
     436             :      * come first.
     437             :      */
     438          42 :     else if (PyBool_Check(obj))
     439             :     {
     440          10 :         out->type = jbvBool;
     441          10 :         out->val.boolean = (obj == Py_True);
     442             :     }
     443          32 :     else if (PyNumber_Check(obj))
     444          32 :         out = PLyNumber_ToJsonbValue(obj, out);
     445             :     else
     446           0 :         ereport(ERROR,
     447             :                 (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
     448             :                  errmsg("Python type \"%s\" cannot be transformed to jsonb",
     449             :                         PLyObject_AsString((PyObject *) obj->ob_type))));
     450             : 
     451          62 :     if (jsonb_state->parseState)
     452             :     {
     453             :         /* We're in an array or object, so push value as element or field. */
     454          50 :         pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, out);
     455             :     }
     456             :     else
     457             :     {
     458             :         /*
     459             :          * We are at top level, so it's a raw scalar.  If we just shove the
     460             :          * scalar value into jsonb_state->result, JsonbValueToJsonb will take
     461             :          * care of wrapping it into a dummy array.
     462             :          */
     463          12 :         jsonb_state->result = out;
     464             :     }
     465             : }
     466             : 
     467             : /*
     468             :  * plpython_to_jsonb
     469             :  *
     470             :  * Transform python object to Jsonb datum
     471             :  */
     472           4 : PG_FUNCTION_INFO_V1(plpython_to_jsonb);
     473             : Datum
     474          44 : plpython_to_jsonb(PG_FUNCTION_ARGS)
     475             : {
     476          44 :     PyObject   *obj = (PyObject *) PG_GETARG_POINTER(0);
     477          44 :     JsonbInState jsonb_state = {0};
     478             : 
     479          44 :     PLyObject_ToJsonbValue(obj, &jsonb_state, true);
     480          42 :     PG_RETURN_POINTER(JsonbValueToJsonb(jsonb_state.result));
     481             : }
     482             : 
     483             : /*
     484             :  * jsonb_to_plpython
     485             :  *
     486             :  * Transform Jsonb datum to PyObject and return it as internal.
     487             :  */
     488           4 : PG_FUNCTION_INFO_V1(jsonb_to_plpython);
     489             : Datum
     490          52 : jsonb_to_plpython(PG_FUNCTION_ARGS)
     491             : {
     492             :     PyObject   *result;
     493          52 :     Jsonb      *in = PG_GETARG_JSONB_P(0);
     494             : 
     495             :     /*
     496             :      * Initialize pointer to Decimal constructor. First we try "cdecimal", C
     497             :      * version of decimal library. In case of failure we use slower "decimal"
     498             :      * module.
     499             :      */
     500          52 :     if (!decimal_constructor)
     501             :     {
     502           2 :         PyObject   *decimal_module = PyImport_ImportModule("cdecimal");
     503             : 
     504           2 :         if (!decimal_module)
     505             :         {
     506           2 :             PyErr_Clear();
     507           2 :             decimal_module = PyImport_ImportModule("decimal");
     508             :         }
     509             :         Assert(decimal_module);
     510           2 :         decimal_constructor = PyObject_GetAttrString(decimal_module, "Decimal");
     511             :     }
     512             : 
     513          52 :     result = PLyObject_FromJsonbContainer(&in->root);
     514          52 :     if (!result)
     515           0 :         PLy_elog(ERROR, "transformation from jsonb to Python failed");
     516             : 
     517          52 :     return PointerGetDatum(result);
     518             : }

Generated by: LCOV version 1.16