summaryrefslogtreecommitdiffstats
path: root/contrib/jsonb_plpython
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:17:33 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:17:33 +0000
commit5e45211a64149b3c659b90ff2de6fa982a5a93ed (patch)
tree739caf8c461053357daa9f162bef34516c7bf452 /contrib/jsonb_plpython
parentInitial commit. (diff)
downloadpostgresql-15-5e45211a64149b3c659b90ff2de6fa982a5a93ed.tar.xz
postgresql-15-5e45211a64149b3c659b90ff2de6fa982a5a93ed.zip
Adding upstream version 15.5.upstream/15.5
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'contrib/jsonb_plpython')
-rw-r--r--contrib/jsonb_plpython/.gitignore4
-rw-r--r--contrib/jsonb_plpython/Makefile34
-rw-r--r--contrib/jsonb_plpython/expected/jsonb_plpython.out306
-rw-r--r--contrib/jsonb_plpython/jsonb_plpython.c504
-rw-r--r--contrib/jsonb_plpython/jsonb_plpython3u--1.0.sql19
-rw-r--r--contrib/jsonb_plpython/jsonb_plpython3u.control6
-rw-r--r--contrib/jsonb_plpython/sql/jsonb_plpython.sql183
7 files changed, 1056 insertions, 0 deletions
diff --git a/contrib/jsonb_plpython/.gitignore b/contrib/jsonb_plpython/.gitignore
new file mode 100644
index 0000000..5dcb3ff
--- /dev/null
+++ b/contrib/jsonb_plpython/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/jsonb_plpython/Makefile b/contrib/jsonb_plpython/Makefile
new file mode 100644
index 0000000..fea7bdf
--- /dev/null
+++ b/contrib/jsonb_plpython/Makefile
@@ -0,0 +1,34 @@
+# contrib/jsonb_plpython/Makefile
+
+MODULE_big = jsonb_plpython$(python_majorversion)
+OBJS = \
+ $(WIN32RES) \
+ jsonb_plpython.o
+PGFILEDESC = "jsonb_plpython - jsonb transform for plpython"
+
+PG_CPPFLAGS = -I$(top_srcdir)/src/pl/plpython $(python_includespec) -DPLPYTHON_LIBNAME='"plpython$(python_majorversion)"'
+
+EXTENSION = jsonb_plpython3u
+DATA = jsonb_plpython3u--1.0.sql
+
+REGRESS = jsonb_plpython
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/jsonb_plpython
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+# We must link libpython explicitly
+ifeq ($(PORTNAME), win32)
+# ... see silliness in plpython Makefile ...
+SHLIB_LINK_INTERNAL += $(sort $(wildcard ../../src/pl/plpython/libpython*.a))
+else
+rpathdir = $(python_libdir)
+SHLIB_LINK += $(python_libspec) $(python_additional_libs)
+endif
diff --git a/contrib/jsonb_plpython/expected/jsonb_plpython.out b/contrib/jsonb_plpython/expected/jsonb_plpython.out
new file mode 100644
index 0000000..cac963d
--- /dev/null
+++ b/contrib/jsonb_plpython/expected/jsonb_plpython.out
@@ -0,0 +1,306 @@
+CREATE EXTENSION jsonb_plpython3u CASCADE;
+NOTICE: installing required extension "plpython3u"
+-- test jsonb -> python dict
+CREATE FUNCTION test1(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, dict)
+assert(val == {'a': 1, 'c': 'NULL'})
+return len(val)
+$$;
+SELECT test1('{"a": 1, "c": "NULL"}'::jsonb);
+ test1
+-------
+ 2
+(1 row)
+
+-- test jsonb -> python dict
+-- complex dict with dicts as value
+CREATE FUNCTION test1complex(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, dict)
+assert(val == {"d": {"d": 1}})
+return len(val)
+$$;
+SELECT test1complex('{"d": {"d": 1}}'::jsonb);
+ test1complex
+--------------
+ 1
+(1 row)
+
+-- test jsonb[] -> python dict
+-- dict with array as value
+CREATE FUNCTION test1arr(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, dict)
+assert(val == {"d": [12, 1]})
+return len(val)
+$$;
+SELECT test1arr('{"d":[12, 1]}'::jsonb);
+ test1arr
+----------
+ 1
+(1 row)
+
+-- test jsonb[] -> python list
+-- simple list
+CREATE FUNCTION test2arr(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, list)
+assert(val == [12, 1])
+return len(val)
+$$;
+SELECT test2arr('[12, 1]'::jsonb);
+ test2arr
+----------
+ 2
+(1 row)
+
+-- test jsonb[] -> python list
+-- array of dicts
+CREATE FUNCTION test3arr(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, list)
+assert(val == [{"a": 1,"b": 2}, {"c": 3,"d": 4}])
+return len(val)
+$$;
+SELECT test3arr('[{"a": 1, "b": 2}, {"c": 3,"d": 4}]'::jsonb);
+ test3arr
+----------
+ 2
+(1 row)
+
+-- test jsonb int -> python int
+CREATE FUNCTION test1int(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert(val == 1)
+return val
+$$;
+SELECT test1int('1'::jsonb);
+ test1int
+----------
+ 1
+(1 row)
+
+-- test jsonb string -> python string
+CREATE FUNCTION test1string(val jsonb) RETURNS text
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert(val == "a")
+return val
+$$;
+SELECT test1string('"a"'::jsonb);
+ test1string
+-------------
+ a
+(1 row)
+
+-- test jsonb null -> python None
+CREATE FUNCTION test1null(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert(val == None)
+return 1
+$$;
+SELECT test1null('null'::jsonb);
+ test1null
+-----------
+ 1
+(1 row)
+
+-- test python -> jsonb
+CREATE FUNCTION roundtrip(val jsonb) RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+as $$
+return val
+$$;
+SELECT roundtrip('null'::jsonb);
+ roundtrip
+-----------
+
+(1 row)
+
+SELECT roundtrip('1'::jsonb);
+ roundtrip
+-----------
+ 1
+(1 row)
+
+SELECT roundtrip('1234567890.0987654321'::jsonb);
+ roundtrip
+-----------------------
+ 1234567890.0987654321
+(1 row)
+
+SELECT roundtrip('-1234567890.0987654321'::jsonb);
+ roundtrip
+------------------------
+ -1234567890.0987654321
+(1 row)
+
+SELECT roundtrip('true'::jsonb);
+ roundtrip
+-----------
+ true
+(1 row)
+
+SELECT roundtrip('"string"'::jsonb);
+ roundtrip
+-----------
+ "string"
+(1 row)
+
+SELECT roundtrip('{"1": null}'::jsonb);
+ roundtrip
+-------------
+ {"1": null}
+(1 row)
+
+SELECT roundtrip('{"1": 1}'::jsonb);
+ roundtrip
+-----------
+ {"1": 1}
+(1 row)
+
+SELECT roundtrip('{"1": true}'::jsonb);
+ roundtrip
+-------------
+ {"1": true}
+(1 row)
+
+SELECT roundtrip('{"1": "string"}'::jsonb);
+ roundtrip
+-----------------
+ {"1": "string"}
+(1 row)
+
+SELECT roundtrip('[null]'::jsonb);
+ roundtrip
+-----------
+ [null]
+(1 row)
+
+SELECT roundtrip('[1]'::jsonb);
+ roundtrip
+-----------
+ [1]
+(1 row)
+
+SELECT roundtrip('[true]'::jsonb);
+ roundtrip
+-----------
+ [true]
+(1 row)
+
+SELECT roundtrip('["string"]'::jsonb);
+ roundtrip
+------------
+ ["string"]
+(1 row)
+
+SELECT roundtrip('[null, 1]'::jsonb);
+ roundtrip
+-----------
+ [null, 1]
+(1 row)
+
+SELECT roundtrip('[1, true]'::jsonb);
+ roundtrip
+-----------
+ [1, true]
+(1 row)
+
+SELECT roundtrip('[true, "string"]'::jsonb);
+ roundtrip
+------------------
+ [true, "string"]
+(1 row)
+
+SELECT roundtrip('["string", "string2"]'::jsonb);
+ roundtrip
+-----------------------
+ ["string", "string2"]
+(1 row)
+
+-- complex numbers -> jsonb
+CREATE FUNCTION testComplexNumbers() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = 1 + 2j
+return x
+$$;
+SELECT testComplexNumbers();
+ERROR: could not convert value "(1+2j)" to jsonb
+CONTEXT: while creating return value
+PL/Python function "testcomplexnumbers"
+-- range -> jsonb
+CREATE FUNCTION testRange() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = range(3)
+return x
+$$;
+SELECT testRange();
+ testrange
+-----------
+ [0, 1, 2]
+(1 row)
+
+-- 0xff -> jsonb
+CREATE FUNCTION testDecimal() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = 0xff
+return x
+$$;
+SELECT testDecimal();
+ testdecimal
+-------------
+ 255
+(1 row)
+
+-- tuple -> jsonb
+CREATE FUNCTION testTuple() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = (1, 'String', None)
+return x
+$$;
+SELECT testTuple();
+ testtuple
+---------------------
+ [1, "String", null]
+(1 row)
+
+-- interesting dict -> jsonb
+CREATE FUNCTION test_dict1() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = {"a": 1, None: 2, 33: 3}
+return x
+$$;
+SELECT test_dict1();
+ test_dict1
+--------------------------
+ {"": 2, "a": 1, "33": 3}
+(1 row)
+
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);
+}
diff --git a/contrib/jsonb_plpython/jsonb_plpython3u--1.0.sql b/contrib/jsonb_plpython/jsonb_plpython3u--1.0.sql
new file mode 100644
index 0000000..60c34c0
--- /dev/null
+++ b/contrib/jsonb_plpython/jsonb_plpython3u--1.0.sql
@@ -0,0 +1,19 @@
+/* contrib/jsonb_plpython/jsonb_plpython3u--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION jsonb_plpython3u" to load this file. \quit
+
+CREATE FUNCTION jsonb_to_plpython3(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'jsonb_to_plpython';
+
+CREATE FUNCTION plpython3_to_jsonb(val internal) RETURNS jsonb
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'plpython_to_jsonb';
+
+CREATE TRANSFORM FOR jsonb LANGUAGE plpython3u (
+ FROM SQL WITH FUNCTION jsonb_to_plpython3(internal),
+ TO SQL WITH FUNCTION plpython3_to_jsonb(internal)
+);
+
+COMMENT ON TRANSFORM FOR jsonb LANGUAGE plpython3u IS 'transform between jsonb and Python';
diff --git a/contrib/jsonb_plpython/jsonb_plpython3u.control b/contrib/jsonb_plpython/jsonb_plpython3u.control
new file mode 100644
index 0000000..f701e80
--- /dev/null
+++ b/contrib/jsonb_plpython/jsonb_plpython3u.control
@@ -0,0 +1,6 @@
+# jsonb_plpython3u extension
+comment = 'transform between jsonb and plpython3u'
+default_version = '1.0'
+module_pathname = '$libdir/jsonb_plpython3'
+relocatable = true
+requires = 'plpython3u'
diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython.sql b/contrib/jsonb_plpython/sql/jsonb_plpython.sql
new file mode 100644
index 0000000..29dc332
--- /dev/null
+++ b/contrib/jsonb_plpython/sql/jsonb_plpython.sql
@@ -0,0 +1,183 @@
+CREATE EXTENSION jsonb_plpython3u CASCADE;
+
+-- test jsonb -> python dict
+CREATE FUNCTION test1(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, dict)
+assert(val == {'a': 1, 'c': 'NULL'})
+return len(val)
+$$;
+
+SELECT test1('{"a": 1, "c": "NULL"}'::jsonb);
+
+-- test jsonb -> python dict
+-- complex dict with dicts as value
+CREATE FUNCTION test1complex(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, dict)
+assert(val == {"d": {"d": 1}})
+return len(val)
+$$;
+
+SELECT test1complex('{"d": {"d": 1}}'::jsonb);
+
+
+-- test jsonb[] -> python dict
+-- dict with array as value
+CREATE FUNCTION test1arr(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, dict)
+assert(val == {"d": [12, 1]})
+return len(val)
+$$;
+
+SELECT test1arr('{"d":[12, 1]}'::jsonb);
+
+-- test jsonb[] -> python list
+-- simple list
+CREATE FUNCTION test2arr(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, list)
+assert(val == [12, 1])
+return len(val)
+$$;
+
+SELECT test2arr('[12, 1]'::jsonb);
+
+-- test jsonb[] -> python list
+-- array of dicts
+CREATE FUNCTION test3arr(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert isinstance(val, list)
+assert(val == [{"a": 1,"b": 2}, {"c": 3,"d": 4}])
+return len(val)
+$$;
+
+SELECT test3arr('[{"a": 1, "b": 2}, {"c": 3,"d": 4}]'::jsonb);
+
+-- test jsonb int -> python int
+CREATE FUNCTION test1int(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert(val == 1)
+return val
+$$;
+
+SELECT test1int('1'::jsonb);
+
+-- test jsonb string -> python string
+CREATE FUNCTION test1string(val jsonb) RETURNS text
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert(val == "a")
+return val
+$$;
+
+SELECT test1string('"a"'::jsonb);
+
+-- test jsonb null -> python None
+CREATE FUNCTION test1null(val jsonb) RETURNS int
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+assert(val == None)
+return 1
+$$;
+
+SELECT test1null('null'::jsonb);
+
+-- test python -> jsonb
+CREATE FUNCTION roundtrip(val jsonb) RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+as $$
+return val
+$$;
+
+SELECT roundtrip('null'::jsonb);
+SELECT roundtrip('1'::jsonb);
+SELECT roundtrip('1234567890.0987654321'::jsonb);
+SELECT roundtrip('-1234567890.0987654321'::jsonb);
+SELECT roundtrip('true'::jsonb);
+SELECT roundtrip('"string"'::jsonb);
+
+SELECT roundtrip('{"1": null}'::jsonb);
+SELECT roundtrip('{"1": 1}'::jsonb);
+SELECT roundtrip('{"1": true}'::jsonb);
+SELECT roundtrip('{"1": "string"}'::jsonb);
+
+SELECT roundtrip('[null]'::jsonb);
+SELECT roundtrip('[1]'::jsonb);
+SELECT roundtrip('[true]'::jsonb);
+SELECT roundtrip('["string"]'::jsonb);
+SELECT roundtrip('[null, 1]'::jsonb);
+SELECT roundtrip('[1, true]'::jsonb);
+SELECT roundtrip('[true, "string"]'::jsonb);
+SELECT roundtrip('["string", "string2"]'::jsonb);
+
+-- complex numbers -> jsonb
+CREATE FUNCTION testComplexNumbers() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = 1 + 2j
+return x
+$$;
+
+SELECT testComplexNumbers();
+
+-- range -> jsonb
+CREATE FUNCTION testRange() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = range(3)
+return x
+$$;
+
+SELECT testRange();
+
+-- 0xff -> jsonb
+CREATE FUNCTION testDecimal() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = 0xff
+return x
+$$;
+
+SELECT testDecimal();
+
+-- tuple -> jsonb
+CREATE FUNCTION testTuple() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = (1, 'String', None)
+return x
+$$;
+
+SELECT testTuple();
+
+-- interesting dict -> jsonb
+CREATE FUNCTION test_dict1() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+x = {"a": 1, None: 2, 33: 3}
+return x
+$$;
+
+SELECT test_dict1();