diff --git a/contrib/Makefile b/contrib/Makefile index e84eb67..d6b7170 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -82,9 +82,9 @@ ALWAYS_SUBDIRS += hstore_plperl endif ifeq ($(with_python),yes) -SUBDIRS += hstore_plpython ltree_plpython +SUBDIRS += hstore_plpython ltree_plpython jsonb_plpython else -ALWAYS_SUBDIRS += hstore_plpython ltree_plpython +ALWAYS_SUBDIRS += hstore_plpython ltree_plpython jsonb_plpython endif # Missing: diff --git a/contrib/jsonb_plpython/Makefile b/contrib/jsonb_plpython/Makefile new file mode 100644 index 0000000..1e34d86 --- /dev/null +++ b/contrib/jsonb_plpython/Makefile @@ -0,0 +1,39 @@ +# contrib/jsonb_plpython/Makefile + +MODULE_big = jsonb_plpython$(python_majorversion)u +OBJS = jsonb_plpython.o $(WIN32RES) +PGFILEDESC = "jsonb_plpython - transform between jsonb and plpythonu" + +PG_CPPFLAGS = -I$(top_srcdir)/src/pl/plpython $(python_includespec) -DPLPYTHON_LIBNAME='"plpython$(python_majorversion)"' + +EXTENSION = jsonb_plpython$(python_majorversion)u +DATA = jsonb_plpython$(python_majorversion)u--1.0.sql + +REGRESS = jsonb_plpython$(python_majorversion) +REGRESS_PLPYTHON3_MANGLE := $(REGRESS) + +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 += $(sort $(wildcard ../../src/pl/plpython/libpython*.a)) +else +rpathdir = $(python_libdir) +SHLIB_LINK += $(python_libspec) $(python_additional_libs) +endif + +ifeq ($(python_majorversion),2) +REGRESS_OPTS += --load-extension=plpython2u +else +REGRESS_OPTS += --load-extension=plpython3u +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..be104af --- /dev/null +++ b/contrib/jsonb_plpython/expected/jsonb_plpython.out @@ -0,0 +1,118 @@ +CREATE EXTENSION jsonb_plpythonu CASCADE; +-- test jsonb -> python dict +CREATE FUNCTION test1(val jsonb) RETURNS int +LANGUAGE plpythonu +TRANSFORM FOR TYPE jsonb +AS $$ +assert isinstance(val, dict) +plpy.info(sorted(val.items())) +return len(val) +$$; +SELECT test1('{"a":1, "c":"NULL"}'::jsonb); +INFO: [('a', Decimal('1')), ('c', 'NULL')] + test1 +------- + 2 +(1 row) + +-- test jsonb -> python dict +-- complex dict with dicts as value +CREATE FUNCTION test1complex(val jsonb) RETURNS int +LANGUAGE plpythonu +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 plpythonu +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 plpythonu +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 plpythonu +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 plpythonu +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == [1]) +return len(val) +$$; +SELECT test1int('1'::jsonb); + test1int +---------- + 1 +(1 row) + +-- test jsonb string -> python string +CREATE FUNCTION test1string(val jsonb) RETURNS int +LANGUAGE plpythonu +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == ["a"]) +return len(val) +$$; +SELECT test1string('"a"'::jsonb); + test1string +------------- + 1 +(1 row) + +DROP EXTENSION jsonb_plpythonu CASCADE; +NOTICE: drop cascades to 7 other objects +DETAIL: drop cascades to function test1(jsonb) +drop cascades to function test1complex(jsonb) +drop cascades to function test1arr(jsonb) +drop cascades to function test2arr(jsonb) +drop cascades to function test3arr(jsonb) +drop cascades to function test1int(jsonb) +drop cascades to function test1string(jsonb) diff --git a/contrib/jsonb_plpython/expected/jsonb_plpython2.out b/contrib/jsonb_plpython/expected/jsonb_plpython2.out new file mode 100644 index 0000000..80afb2d --- /dev/null +++ b/contrib/jsonb_plpython/expected/jsonb_plpython2.out @@ -0,0 +1,119 @@ +CREATE EXTENSION jsonb_plpython2u CASCADE; +-- test jsonb -> python dict +CREATE FUNCTION test1(val jsonb) RETURNS int +LANGUAGE plpython2u +TRANSFORM FOR TYPE jsonb +AS $$ +assert isinstance(val, dict) +plpy.info(sorted(val.items())) +return len(val) +$$; +SELECT test1('{"a":1, "c":"NULL"}'::jsonb); +INFO: [('a', Decimal('1')), ('c', 'NULL')] + test1 +------- + 2 +(1 row) + +-- test jsonb -> python dict +-- complex dict with dicts as value +CREATE FUNCTION test1complex(val jsonb) RETURNS int +LANGUAGE plpython2u +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 plpython2u +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 plpython2u +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 plpython2u +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 plpython2u +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == [1]) +return len(val) +$$; +SELECT test1int('1'::jsonb); + test1int +---------- + 1 +(1 row) + +-- test jsonb string -> python string +CREATE FUNCTION test1string(val jsonb) RETURNS int +LANGUAGE plpython2u +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == ["a"]) +return len(val) +$$; +SELECT test1string('"a"'::jsonb); + test1string +------------- + 1 +(1 row) + +DROP EXTENSION plpython2u CASCADE; +NOTICE: drop cascades to 8 other objects +DETAIL: drop cascades to extension jsonb_plpython2u +drop cascades to function test1(jsonb) +drop cascades to function test1complex(jsonb) +drop cascades to function test1arr(jsonb) +drop cascades to function test2arr(jsonb) +drop cascades to function test3arr(jsonb) +drop cascades to function test1int(jsonb) +drop cascades to function test1string(jsonb) diff --git a/contrib/jsonb_plpython/expected/jsonb_plpython3.out b/contrib/jsonb_plpython/expected/jsonb_plpython3.out new file mode 100644 index 0000000..80202b0 --- /dev/null +++ b/contrib/jsonb_plpython/expected/jsonb_plpython3.out @@ -0,0 +1,119 @@ +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) +plpy.info(sorted(val.items())) +return len(val) +$$; +SELECT test1('{"a":1, "c":"NULL"}'::jsonb); +INFO: [('a', Decimal('1')), ('c', 'NULL')] + 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 len(val) +$$; +SELECT test1int('1'::jsonb); + test1int +---------- + 1 +(1 row) + +-- test jsonb string -> python string +CREATE FUNCTION test1string(val jsonb) RETURNS int +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == ["a"]) +return len(val) +$$; +SELECT test1string('"a"'::jsonb); + test1string +------------- + 1 +(1 row) + +DROP EXTENSION plpython3u CASCADE; +NOTICE: drop cascades to 8 other objects +DETAIL: drop cascades to extension jsonb_plpython3u +drop cascades to function test1(jsonb) +drop cascades to function test1complex(jsonb) +drop cascades to function test1arr(jsonb) +drop cascades to function test2arr(jsonb) +drop cascades to function test3arr(jsonb) +drop cascades to function test1int(jsonb) +drop cascades to function test1string(jsonb) diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c new file mode 100644 index 0000000..97960ac --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -0,0 +1,417 @@ +/* This document contains an implementation of transformations from python + * object to jsonb and vise versa. + * In this file you can find implementation of transformations: + * - JsonbValue transformation in PyObject_FromJsonbValue + * - JsonbContainer(jsonb) transformation in PyObject_FromJsonb + * - PyMapping object(dict) transformation in PyMapping_ToJsonbValue + * - PyString object transformation in PyString_ToJsonbValue + * - PySequence object(list) transformation in PySequence_ToJsonbValue + * - PyNumeric object transformation in PyNumeric_ToJsonbValue + * - PyMapping object transformation in PyObject_ToJsonbValue + * */ +#include "postgres.h" + +#include "plpython.h" +#include "plpy_typeio.h" + +#include "utils/jsonb.h" +#include "utils/fmgrprotos.h" + +PG_MODULE_MAGIC; + +extern void _PG_init(void); + +/* Linkage to functions in plpython module */ +typedef char *(*PLyObject_AsString_t) (PyObject *plrv); +static PLyObject_AsString_t PLyObject_AsString_p; +#if PY_MAJOR_VERSION >= 3 +typedef PyObject *(*PLyUnicode_FromStringAndSize_t) (const char *s, Py_ssize_t size); +static PLyUnicode_FromStringAndSize_t PLyUnicode_FromStringAndSize_p; +#endif + +/* + * 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); +#if PY_MAJOR_VERSION >= 3 + AssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); + PLyUnicode_FromStringAndSize_p = (PLyUnicode_FromStringAndSize_t) + load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyUnicode_FromStringAndSize", + true, NULL); +#endif +} + + +/* These defines must be after the module init function */ +#define PLyObject_AsString PLyObject_AsString_p +#define PLyUnicode_FromStringAndSize PLyUnicode_FromStringAndSize_p + +/* + * decimal_constructor is a link to Python library + * for transforming strings into python decimal type + * */ +static PyObject *decimal_constructor; + +static PyObject *PyObject_FromJsonb(JsonbContainer *jsonb); +static JsonbValue *PyObject_ToJsonbValue(PyObject *obj, JsonbParseState *jsonb_state); + +/* + * PyObject_FromJsonbValue(JsonsValue *jsonbValue) + * Function for transforming JsonbValue type into Python Object + * The first argument defines the JsonbValue which will be transformed into PyObject + * Return value is the pointer to Jsonb structure containing the transformed object. + * */ +static PyObject * +PyObject_FromJsonbValue(JsonbValue *jsonbValue) +{ + PyObject *result; + char *str; + + switch (jsonbValue->type) + { + case jbvNull: + result = Py_None; + break; + case jbvBinary: + result = PyObject_FromJsonb(jsonbValue->val.binary.data); + break; + case jbvNumeric: + + /* + * XXX There should be a better way. Right now Numeric is + * transformed into string and then this string is parsed into py + * numeric + */ + str = DatumGetCString( + DirectFunctionCall1(numeric_out, NumericGetDatum(jsonbValue->val.numeric)) + ); + result = PyObject_CallFunction(decimal_constructor, "s", str); + break; + case jbvString: + result = PyString_FromStringAndSize( + jsonbValue->val.string.val, + jsonbValue->val.string.len + ); + break; + case jbvBool: + result = jsonbValue->val.boolean ? Py_True : Py_False; + break; + case jbvArray: + case jbvObject: + result = PyObject_FromJsonb(jsonbValue->val.binary.data); + break; + } + return (result); +} + +/* + * PyObject_FromJsonb(JsonbContainer *jsonb) + * Function for transforming JsonbContainer(jsonb) into PyObject + * The first argument should represent the data for transformation. + * Return value is the pointer to Python object. + * */ + +static PyObject * +PyObject_FromJsonb(JsonbContainer *jsonb) +{ + PyObject *object = Py_None; + JsonbIterator *it; + JsonbIteratorToken r; + JsonbValue v; + + object = PyDict_New(); + it = JsonbIteratorInit(jsonb); + + /* + * Iterate trhrough Jsonb object. + */ + while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE) + { + PyObject *key = Py_None; + PyObject *value = Py_None; + + switch (r) + { + case (WJB_KEY): + /* dict key in v */ + key = PyString_FromStringAndSize( + v.val.string.val, + v.val.string.len + ); + + r = JsonbIteratorNext(&it, &v, true); + value = PyObject_FromJsonbValue(&v); + PyDict_SetItem(object, key, value); + break; + case (WJB_BEGIN_ARRAY): + /* array in v */ + object = PyList_New(0); + while ( + ((r = JsonbIteratorNext(&it, &v, true)) == WJB_ELEM) + && (r != WJB_DONE) + ) + PyList_Append(object, PyObject_FromJsonbValue(&v)); + return (object); + break; + case (WJB_END_OBJECT): + case (WJB_BEGIN_OBJECT): + /* no object are in v */ + break; + default: + /* simple objects */ + object = PyObject_FromJsonbValue(&v); + break; + } + Py_XDECREF(value); + Py_XDECREF(key); + } + return (object); +} + + +/* + * jsonb_to_plpython(Jsonb *in) + * Function to transform jsonb object to corresponding python object. + * The first argument is the Jsonb object to be transformed. + * Return value is the pointer to Python object. + * */ +PG_FUNCTION_INFO_V1(jsonb_to_plpython); +Datum +jsonb_to_plpython(PG_FUNCTION_ARGS) +{ + Jsonb *in; + PyObject *dict; + PyObject *decimal_module; + + in = PG_GETARG_JSONB_P(0); + + /* Import python cdecimal library and if there is no cdecimal library, */ + /* import decimal library */ + if (!decimal_constructor) + { + decimal_module = PyImport_ImportModule("cdecimal"); + if (!decimal_module) + { + PyErr_Clear(); + decimal_module = PyImport_ImportModule("decimal"); + } + decimal_constructor = PyObject_GetAttrString(decimal_module, "Decimal"); + } + + dict = PyObject_FromJsonb(&in->root); + return PointerGetDatum(dict); +} + + +/* + * PyMapping_ToJsonbValue(PyObject *obj, JsonbParseState *jsonb_state) + * Function to transform Python lists to jsonbValue + * The first argument is the python object to be transformed. + * Return value is the pointer to JsonbValue structure containing the list. + * */ +static JsonbValue * +PyMapping_ToJsonbValue(PyObject *obj, JsonbParseState *jsonb_state) +{ + volatile PyObject *items_v = NULL; + int32 pcount; + JsonbValue *out = NULL; + + pcount = PyMapping_Size(obj); + items_v = PyMapping_Items(obj); + + PG_TRY(); + { + int32 i; + PyObject *items; + JsonbValue *jbvValue; + JsonbValue jbvKey; + + items = (PyObject *) items_v; + pushJsonbValue(&jsonb_state, WJB_BEGIN_OBJECT, NULL); + + for (i = 0; i < pcount; i++) + { + PyObject *tuple; + PyObject *key; + PyObject *value; + + tuple = PyList_GetItem(items, i); + key = PyTuple_GetItem(tuple, 0); + value = PyTuple_GetItem(tuple, 1); + + if (key == Py_None) + { + jbvKey.type = jbvString; + jbvKey.val.string.len = 0; + jbvKey.val.string.val = ""; + } + else + { + jbvKey.type = jbvString; + jbvKey.val.string.val = PLyObject_AsString(key); + jbvKey.val.string.len = strlen(jbvKey.val.string.val); + } + pushJsonbValue(&jsonb_state, WJB_KEY, &jbvKey); + jbvValue = PyObject_ToJsonbValue(value, jsonb_state); + if (IsAJsonbScalar(jbvValue)) + pushJsonbValue(&jsonb_state, WJB_VALUE, jbvValue); + } + out = pushJsonbValue(&jsonb_state, WJB_END_OBJECT, NULL); + } + PG_CATCH(); + { + Py_DECREF(items_v); + PG_RE_THROW(); + } + PG_END_TRY(); + return (out); +} + +/* + * PyString_ToJsonbValue(PyObject *obj) + * Function to transform python string object to jsonbValue object. + * The first argument is the Python String object to be transformed. + * Return value is the pointer to JsonbValue structure containing the String. + * */ +static JsonbValue * +PyString_ToJsonbValue(PyObject *obj) +{ + JsonbValue *out = NULL; + JsonbValue *jbvElem; + + jbvElem = palloc(sizeof(JsonbValue)); + jbvElem->type = jbvString; + jbvElem->val.string.val = PLyObject_AsString(obj); + jbvElem->val.string.len = strlen(jbvElem->val.string.val); + out = jbvElem; + + return (out); +} + +/* + * PySequence_ToJsonbValue(PyObject *obj, JsonbParseState *jsonb_state) + * Function to transform python lists to jsonbValue object. + * The first argument is the Python list to be transformed. + * The second one is conversion state. + * Return value is the pointer to JsonbValue structure containing array. + * */ +static JsonbValue * +PySequence_ToJsonbValue(PyObject *obj, JsonbParseState *jsonb_state) +{ + JsonbValue *jbvElem; + JsonbValue *out = NULL; + int32 pcount; + int32 i; + + pcount = PySequence_Size(obj); + + + pushJsonbValue(&jsonb_state, WJB_BEGIN_ARRAY, NULL); + + for (i = 0; i < pcount; i++) + { + PyObject *value; + + value = PySequence_GetItem(obj, i); + jbvElem = PyObject_ToJsonbValue(value, jsonb_state); + if (IsAJsonbScalar(jbvElem)) + pushJsonbValue(&jsonb_state, WJB_ELEM, jbvElem); + } + out = pushJsonbValue(&jsonb_state, WJB_END_ARRAY, NULL); + return (out); +} + +/* + * PyNumeric_ToJsonbValue(PyObject *obj) + * Function to transform python numerics to jsonbValue object. + * The first argument is the Python numeric object to be transformed. + * Return value is the pointer to JsonbValue structure containing the String. + * */ +static JsonbValue * +PyNumeric_ToJsonbValue(PyObject *obj) +{ + JsonbValue *out = NULL; + JsonbValue *jbvInt; + + jbvInt = palloc(sizeof(JsonbValue)); + jbvInt->type = jbvNumeric; + jbvInt->val.numeric = DatumGetNumeric(DirectFunctionCall1( + numeric_in, + CStringGetDatum(PLyObject_AsString(obj)) + )); + out = jbvInt; + return (out); +} + +/* + * PyObject_ToJsonbValue(PyObject *obj) + * Function to transform python objects to jsonbValue object. + * The first argument is the Python object to be transformed. + * Return value is the pointer to JsonbValue structure containing the transformed object. + * */ +static JsonbValue * +PyObject_ToJsonbValue(PyObject *obj, JsonbParseState *jsonb_state) +{ + JsonbValue *out = NULL; + + if (PyMapping_Check(obj)) + { + /* DICT */ + out = PyMapping_ToJsonbValue(obj, jsonb_state); + } + else if (PyString_Check(obj)) + { + /* STRING */ + out = PyString_ToJsonbValue(obj); + } + else if (PySequence_Check(obj)) + { + /* LIST or STRING */ + /* but we have checked on STRING */ + out = PySequence_ToJsonbValue(obj, jsonb_state); + } + else if (PyNumber_Check(obj)) + { + /* NUMERIC */ + out = PyNumeric_ToJsonbValue(obj); + } + else + { + /* EVERYTHING ELSE */ + /* Handle it as it's repr */ + JsonbValue *jbvElem; + + jbvElem = palloc(sizeof(JsonbValue)); + jbvElem->type = jbvString; + jbvElem->val.string.val = PLyObject_AsString(obj); + jbvElem->val.string.len = strlen(jbvElem->val.string.val); + out = jbvElem; + } + return (out); +} + +/* + * plpython_to_jsonb(PyObject *obj) + * Function to transform python objects to jsonb object. + * The first argument is the Python object to be transformed. + * Return value is the pointer to Jsonb structure containing the transformed object. + * */ +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 = PyObject_ToJsonbValue(obj, jsonb_state); + PG_RETURN_POINTER(JsonbValueToJsonb(out)); +} diff --git a/contrib/jsonb_plpython/jsonb_plpython2u--1.0.sql b/contrib/jsonb_plpython/jsonb_plpython2u--1.0.sql new file mode 100644 index 0000000..1e38847 --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpython2u--1.0.sql @@ -0,0 +1,19 @@ +/* contrib/jsonb_plpython/jsonb_plpythonu--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION jsonb_plpython" to load this file. \quit + +CREATE FUNCTION jsonb_to_plpython(val internal) RETURNS internal +LANGUAGE C STRICT IMMUTABLE +AS 'MODULE_PATHNAME'; + +CREATE FUNCTION plpython_to_jsonb(val internal) RETURNS jsonb +LANGUAGE C STRICT IMMUTABLE +AS 'MODULE_PATHNAME'; + +CREATE TRANSFORM FOR jsonb LANGUAGE plpython2u ( + FROM SQL WITH FUNCTION jsonb_to_plpython(internal), + TO SQL WITH FUNCTION plpython_to_jsonb(internal) +); + +COMMENT ON TRANSFORM FOR jsonb LANGUAGE plpython2u IS 'transform between jsonb and Python dict'; diff --git a/contrib/jsonb_plpython/jsonb_plpython2u.control b/contrib/jsonb_plpython/jsonb_plpython2u.control new file mode 100644 index 0000000..3514cbc --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpython2u.control @@ -0,0 +1,6 @@ +# jsonb_plpython2u extension +comment = 'transform between jsonb and plpythonu' +default_version = '1.0' +module_pathname = '$libdir/jsonb_plpython2u' +relocatable = true +requires = 'plpython2u' 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..0958db7 --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpython3u--1.0.sql @@ -0,0 +1,19 @@ +/* contrib/jsonb_plpython/jsonb_plpythonu--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION jsonb_plpython3" to load this file. \quit + +CREATE FUNCTION jsonb_to_plpython(val internal) RETURNS internal +LANGUAGE C STRICT IMMUTABLE +AS 'MODULE_PATHNAME'; + +CREATE FUNCTION plpython_to_jsonb(val internal) RETURNS jsonb +LANGUAGE C STRICT IMMUTABLE +AS 'MODULE_PATHNAME'; + +CREATE TRANSFORM FOR jsonb LANGUAGE plpython3u ( + FROM SQL WITH FUNCTION jsonb_to_plpython(internal), + TO SQL WITH FUNCTION plpython_to_jsonb(internal) +); + +COMMENT ON TRANSFORM FOR jsonb LANGUAGE plpython3u IS 'transform between jsonb and Python dict'; diff --git a/contrib/jsonb_plpython/jsonb_plpython3u.control b/contrib/jsonb_plpython/jsonb_plpython3u.control new file mode 100644 index 0000000..bbf6ed7 --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpython3u.control @@ -0,0 +1,6 @@ +# jsonb_plpython3u extension +comment = 'transform between jsonb and plpythonu' +default_version = '1.0' +module_pathname = '$libdir/jsonb_plpython3' +relocatable = false +requires = 'plpython3u' diff --git a/contrib/jsonb_plpython/jsonb_plpythonu--1.0.sql b/contrib/jsonb_plpython/jsonb_plpythonu--1.0.sql new file mode 100644 index 0000000..7c9460d --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpythonu--1.0.sql @@ -0,0 +1,19 @@ +/* contrib/jsonb_plpython/jsonb_plpythonu--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION jsonb_plpython" to load this file. \quit + +CREATE FUNCTION jsonb_to_plpython(val internal) RETURNS internal +LANGUAGE C STRICT IMMUTABLE +AS 'MODULE_PATHNAME'; + +CREATE FUNCTION plpython_to_jsonb(val internal) RETURNS jsonb +LANGUAGE C STRICT IMMUTABLE +AS 'MODULE_PATHNAME'; + +CREATE TRANSFORM FOR jsonb LANGUAGE plpythonu ( + FROM SQL WITH FUNCTION jsonb_to_plpython(internal), + TO SQL WITH FUNCTION plpython_to_jsonb(internal) +); + +COMMENT ON TRANSFORM FOR jsonb LANGUAGE plpythonu IS 'transform between jsonb and Python dict'; diff --git a/contrib/jsonb_plpython/jsonb_plpythonu.control b/contrib/jsonb_plpython/jsonb_plpythonu.control new file mode 100644 index 0000000..d696dfb --- /dev/null +++ b/contrib/jsonb_plpython/jsonb_plpythonu.control @@ -0,0 +1,6 @@ +# jsonb_plpython2u extension +comment = 'transform between jsonb and plpythonu' +default_version = '1.0' +module_pathname = '$libdir/jsonb_plpython' +relocatable = true +requires = 'plpython2u' diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython.sql b/contrib/jsonb_plpython/sql/jsonb_plpython.sql new file mode 100644 index 0000000..dda714e --- /dev/null +++ b/contrib/jsonb_plpython/sql/jsonb_plpython.sql @@ -0,0 +1,91 @@ +CREATE EXTENSION jsonb_plpythonu CASCADE; + +-- test jsonb -> python dict +CREATE FUNCTION test1(val jsonb) RETURNS int +LANGUAGE plpythonu +TRANSFORM FOR TYPE jsonb +AS $$ +assert isinstance(val, dict) +plpy.info(sorted(val.items())) +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 plpythonu +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 plpythonu +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 plpythonu +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 plpythonu +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 plpythonu +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == [1]) +return len(val) +$$; + +SELECT test1int('1'::jsonb); + +-- test jsonb string -> python string +CREATE FUNCTION test1string(val jsonb) RETURNS int +LANGUAGE plpythonu +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == ["a"]) +return len(val) +$$; + +SELECT test1string('"a"'::jsonb); + + +DROP EXTENSION plpython cascade; diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython2.sql b/contrib/jsonb_plpython/sql/jsonb_plpython2.sql new file mode 100644 index 0000000..96478a2 --- /dev/null +++ b/contrib/jsonb_plpython/sql/jsonb_plpython2.sql @@ -0,0 +1,91 @@ +CREATE EXTENSION jsonb_plpython2u CASCADE; + +-- test jsonb -> python dict +CREATE FUNCTION test1(val jsonb) RETURNS int +LANGUAGE plpython2u +TRANSFORM FOR TYPE jsonb +AS $$ +assert isinstance(val, dict) +plpy.info(sorted(val.items())) +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 plpython2u +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 plpython2u +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 plpython2u +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 plpython2u +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 plpython2u +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == [1]) +return len(val) +$$; + +SELECT test1int('1'::jsonb); + +-- test jsonb string -> python string +CREATE FUNCTION test1string(val jsonb) RETURNS int +LANGUAGE plpython2u +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == ["a"]) +return len(val) +$$; + +SELECT test1string('"a"'::jsonb); + + +DROP EXTENSION plpython2u CASCADE; diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython3.sql b/contrib/jsonb_plpython/sql/jsonb_plpython3.sql new file mode 100644 index 0000000..80e22a1 --- /dev/null +++ b/contrib/jsonb_plpython/sql/jsonb_plpython3.sql @@ -0,0 +1,91 @@ +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) +plpy.info(sorted(val.items())) +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 len(val) +$$; + +SELECT test1int('1'::jsonb); + +-- test jsonb string -> python string +CREATE FUNCTION test1string(val jsonb) RETURNS int +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +assert(val == ["a"]) +return len(val) +$$; + +SELECT test1string('"a"'::jsonb); + + +DROP EXTENSION plpython3u CASCADE; diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 3cf78d6..66d307c 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -569,4 +569,20 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu compared using the default database collation. + + Transforms + + + Additional extensions are available that implement transforms for + the jsonb type for the language PL/Python. The + extensions for PL/Perl are called + jsonb_plpythonu, jsonb_plpython2u, + and jsonb_plpython3u + (see for the PL/Python naming + convention). If you use them, jsonb values are mapped to + Python dictionaries. + + + +