mirror of
https://github.com/postgres/postgres.git
synced 2026-07-07 08:41:01 -04:00
PL/Python and its hstore and jsonb transforms build SQL values from Python containers by calling Python C API functions that can return NULL, and in several places the result was used without first checking it. On the sequence side, PySequence_GetItem() is used when converting a returned sequence into a SQL array or composite value, when reading the argument list passed to plpy.execute() or plpy.cursor(), and when reading the list of type names given to plpy.prepare(). On the mapping side, the hstore and jsonb transforms call PyMapping_Size() and PyMapping_Items() and then index the result with PyList_GetItem() and PyTuple_GetItem(). All of these return NULL (or -1), with a Python exception set, for a broken object: for example one whose __getitem__() or items() raises, or which reports a length that disagrees with what it actually yields. The unchecked result was then dereferenced, crashing the backend. Fix this by checking the result of each call and reporting a regular error if it failed, so that the underlying Python exception is surfaced instead of taking down the session. Author: Richard Guo <guofenglinux@gmail.com> Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com> Discussion: https://postgr.es/m/CAMbWs49BKM9wP6m8bCXEpHwQKp7usvOGV6Jf=J7FYr_BCpxLqg@mail.gmail.com Backpatch-through: 14
395 lines
7.8 KiB
Text
395 lines
7.8 KiB
Text
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)
|
|
|
|
-- A custom sequence whose __getitem__ raises should be reported as an error,
|
|
-- not crash the backend
|
|
CREATE FUNCTION test_broken_sequence() RETURNS jsonb
|
|
LANGUAGE plpython3u
|
|
TRANSFORM FOR TYPE jsonb
|
|
AS $$
|
|
class C:
|
|
def __len__(self):
|
|
return 2
|
|
def __getitem__(self, i):
|
|
raise ValueError('getitem failed')
|
|
return C()
|
|
$$;
|
|
SELECT test_broken_sequence();
|
|
ERROR: could not get element 0 from sequence
|
|
DETAIL: ValueError: getitem failed
|
|
CONTEXT: Traceback (most recent call last):
|
|
while creating return value
|
|
PL/Python function "test_broken_sequence"
|
|
-- A mapping whose items() raises should be reported as an error, not crash
|
|
-- the backend
|
|
CREATE FUNCTION test_broken_mapping() RETURNS jsonb
|
|
LANGUAGE plpython3u
|
|
TRANSFORM FOR TYPE jsonb
|
|
AS $$
|
|
class C(dict):
|
|
def items(self):
|
|
raise ValueError('items failed')
|
|
d = C()
|
|
d['x'] = 1
|
|
return d
|
|
$$;
|
|
SELECT test_broken_mapping();
|
|
ERROR: could not get items from Python mapping
|
|
DETAIL: ValueError: items failed
|
|
CONTEXT: Traceback (most recent call last):
|
|
while creating return value
|
|
PL/Python function "test_broken_mapping"
|
|
-- Likewise for a mapping whose items() does not return key/value pairs
|
|
CREATE FUNCTION test_malformed_mapping() RETURNS jsonb
|
|
LANGUAGE plpython3u
|
|
TRANSFORM FOR TYPE jsonb
|
|
AS $$
|
|
class C(dict):
|
|
def items(self):
|
|
return [42]
|
|
d = C()
|
|
d['x'] = 1
|
|
return d
|
|
$$;
|
|
SELECT test_malformed_mapping();
|
|
ERROR: items() of a Python mapping must return key/value pairs
|
|
CONTEXT: while creating return value
|
|
PL/Python function "test_malformed_mapping"
|
|
-- Likewise for a mapping whose items() yields fewer pairs than its length
|
|
CREATE FUNCTION test_short_mapping() RETURNS jsonb
|
|
LANGUAGE plpython3u
|
|
TRANSFORM FOR TYPE jsonb
|
|
AS $$
|
|
class C(dict):
|
|
def items(self):
|
|
return []
|
|
d = C()
|
|
d['x'] = 1
|
|
return d
|
|
$$;
|
|
SELECT test_short_mapping();
|
|
ERROR: items() of a Python mapping must return key/value pairs
|
|
DETAIL: IndexError: list index out of range
|
|
CONTEXT: while creating return value
|
|
PL/Python function "test_short_mapping"
|
|
-- Likewise for a mapping whose __len__() raises
|
|
CREATE FUNCTION test_broken_len_mapping() RETURNS jsonb
|
|
LANGUAGE plpython3u
|
|
TRANSFORM FOR TYPE jsonb
|
|
AS $$
|
|
class C(dict):
|
|
def __len__(self):
|
|
raise ValueError('len failed')
|
|
d = C()
|
|
d['x'] = 1
|
|
return d
|
|
$$;
|
|
SELECT test_broken_len_mapping();
|
|
ERROR: could not get size of Python mapping
|
|
DETAIL: ValueError: len failed
|
|
CONTEXT: Traceback (most recent call last):
|
|
while creating return value
|
|
PL/Python function "test_broken_len_mapping"
|