Skip to content

Commit aebb975

Browse files
authored
Merge pull request #59 from pgsql-io/py-3.11
Python 3.11 support
2 parents b0a274c + 3d4a15f commit aebb975

File tree

8 files changed

+296
-8
lines changed

8 files changed

+296
-8
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ done so far:
1111
- Add support for bulk_insert FDWs on PG14+ (https://github.com/pgsql-io/multicorn2/pull/45)
1212
- PG16: Fix compatibility issues w/ log_to_postgres and join query planning in PostgreSQL 16 (https://github.com/pgsql-io/multicorn2/pull/51)
1313
- Fix crashes in EXPLAIN with complex quals (https://github.com/pgsql-io/multicorn2/pull/54)
14+
- Support Python 3.11 (https://github.com/pgsql-io/multicorn2/pull/59)
15+
- Behavior change: When log_to_postgres with level ERROR or FATAL is invoked, a specialized Python exception will be thrown and the stack unwound, allowing `catch` and `finally` blocks, and other things like context handler exits, to be invoked in the FDW. (https://github.com/pgsql-io/multicorn2/pull/59)
1416

1517
to do:
1618
- confirm support for Python 3.11 & 3.12

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Multicorn2
33
==========
44

5-
Multicorn Python3 Foreign Data Wrapper (FDW) for Postgresql. Tested on Linux w/ Python 3.9-3.12 & Postgres 12-17.
5+
Multicorn Python3 Foreign Data Wrapper (FDW) for Postgresql. Tested on Linux w/ Python 3.9-3.11 & Postgres 12-17.
66

77
The Multicorn Foreign Data Wrapper allows you to fetch foreign data in Python in your PostgreSQL server.
88

@@ -151,7 +151,7 @@ nix build .#testSuites.test_pg12_py39
151151
```
152152

153153
**Known issues:**
154-
- The tests cover only the supported range of Python & PostgreSQL combinations; in particular, Python releases 3.11 and later are disabled due to failures that have not been addressed.
154+
- The tests cover only the supported range of Python & PostgreSQL combinations; in particular, Python releases 3.12 and later are disabled due to failures that have not been addressed.
155155

156156
### Adding new Python or PostgreSQL versions to the test suite
157157

flake.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
testPythonVersions = with pkgs; [
2525
python39
2626
python310
27-
# python311 # tests are currently broken
27+
python311
2828
# python312 # tests are currently broken
2929
# python313 # tests are currently broken
3030
];
@@ -127,6 +127,7 @@
127127
./Makefile
128128
./test-3.9
129129
./test-3.10
130+
./test-3.11
130131
./test-common
131132
];
132133
unpackPhase = ''

python/multicorn/utils.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,25 @@ def _log_to_postgres(message, level=0, hint=None, detail=None):
2020
}
2121

2222

23+
class MulticornException(Exception):
24+
def __init__(self, message, code, hint, detail):
25+
self._is_multicorn_exception = True
26+
self.message = message
27+
self.code = code
28+
self.hint = hint
29+
self.detail = detail
30+
31+
2332
def log_to_postgres(message, level=INFO, hint=None, detail=None):
2433
code = REPORT_CODES.get(level, None)
2534
if code is None:
2635
raise KeyError("Not a valid log level")
27-
_log_to_postgres(message, code, hint=hint, detail=detail)
28-
36+
if level in (ERROR, CRITICAL):
37+
# if we sent an ERROR or FATAL(=CRITICAL) message to _log_to_postgres, we would trigger the PostgreSQL C-level
38+
# exception handling, which would prevent us from cleanly exiting whatever Python context we're currently in.
39+
# To avoid this, these log levels are replaced with exceptions which are bubbled back to Multicorn's entry
40+
# points, and those exceptions are translated into appropriate logging after we exit the method at the top of
41+
# the multicorn stack.
42+
raise MulticornException(message, code, hint, detail)
43+
else:
44+
_log_to_postgres(message, code, hint=hint, detail=detail)

src/errors.c

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ void reportException(PyObject *pErrType,
1717
PyObject *pErrValue,
1818
PyObject *pErrTraceback);
1919

20+
void reportMulticornException(PyObject *pErrValue);
2021

2122
PGDLLEXPORT void
2223
errorCheck()
@@ -28,7 +29,22 @@ errorCheck()
2829
PyErr_Fetch(&pErrType, &pErrValue, &pErrTraceback);
2930
if (pErrType)
3031
{
31-
reportException(pErrType, pErrValue, pErrTraceback);
32+
// if the error value has a property _is_multicorn_exception and a boolean value True, then we don't report the
33+
// error as a generic exception with a stack trace -- instead we just take the message, code(severity), hint,
34+
// and detail, and log it to Postgres. These exceptions are generated in utils.py to intercept ERROR/FATAL log
35+
// messages. So, first detect whether that's the case, and call a new reporting function...
36+
PyObject *is_multicorn_exception = PyObject_GetAttrString(pErrValue, "_is_multicorn_exception");
37+
if (is_multicorn_exception != NULL && PyObject_IsTrue(is_multicorn_exception))
38+
{
39+
Py_DECREF(is_multicorn_exception);
40+
Py_DECREF(pErrType);
41+
Py_DECREF(pErrTraceback);
42+
reportMulticornException(pErrValue);
43+
}
44+
else
45+
{
46+
reportException(pErrType, pErrValue, pErrTraceback);
47+
}
3248
}
3349
}
3450

@@ -92,3 +108,64 @@ reportException(PyObject *pErrType, PyObject *pErrValue, PyObject *pErrTraceback
92108
errfinish(0);
93109
#endif
94110
}
111+
112+
void reportMulticornException(PyObject* pErrValue)
113+
{
114+
int severity;
115+
PyObject *message = PyObject_GetAttrString(pErrValue, "message");
116+
PyObject *hint = PyObject_GetAttrString(pErrValue, "hint");
117+
PyObject *detail = PyObject_GetAttrString(pErrValue, "detail");
118+
PyObject *code = PyObject_GetAttrString(pErrValue, "code");
119+
int level = PyLong_AsLong(code);
120+
121+
// Matches up with REPORT_CODES in utils.py
122+
switch (level)
123+
{
124+
case 3:
125+
severity = ERROR;
126+
break;
127+
default:
128+
case 4:
129+
severity = FATAL;
130+
break;
131+
}
132+
133+
PG_TRY();
134+
{
135+
136+
#if PG_VERSION_NUM >= 130000
137+
if (errstart(severity, TEXTDOMAIN))
138+
#else
139+
if (errstart(severity, __FILE__, __LINE__, PG_FUNCNAME_MACRO, TEXTDOMAIN))
140+
#endif
141+
{
142+
errmsg("%s", PyString_AsString(message));
143+
if (hint != NULL && hint != Py_None)
144+
{
145+
char* hintstr = PyString_AsString(hint);
146+
errhint("%s", hintstr);
147+
}
148+
if (detail != NULL && detail != Py_None)
149+
{
150+
char* detailstr = PyString_AsString(detail);
151+
errdetail("%s", detailstr);
152+
}
153+
#if PG_VERSION_NUM >= 130000
154+
errfinish(__FILE__, __LINE__, PG_FUNCNAME_MACRO);
155+
#else
156+
errfinish(0);
157+
#endif
158+
}
159+
160+
}
161+
PG_CATCH();
162+
{
163+
Py_DECREF(message);
164+
Py_DECREF(hint);
165+
Py_DECREF(detail);
166+
Py_DECREF(code);
167+
Py_DECREF(pErrValue);
168+
PG_RE_THROW();
169+
}
170+
PG_END_TRY();
171+
}

src/python.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,6 @@ getCacheEntry(Oid foreigntableid)
667667
MemoryContextDelete(tempContext);
668668
}
669669
RelationClose(rel);
670-
Py_INCREF(entry->value);
671670

672671
/*
673672
* Start a new transaction or subtransaction if needed.
@@ -685,7 +684,9 @@ getCacheEntry(Oid foreigntableid)
685684
PyObject *
686685
getInstance(Oid foreigntableid)
687686
{
688-
return getCacheEntry(foreigntableid)->value;
687+
PyObject* retval = getCacheEntry(foreigntableid)->value;
688+
Py_INCREF(retval);
689+
return retval;
689690
}
690691

691692

test-3.11

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test-3.9

test-3.9/expected/write_test_3.out

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
CREATE EXTENSION multicorn;
2+
CREATE server multicorn_srv foreign data wrapper multicorn options (
3+
wrapper 'multicorn.testfdw.TestForeignDataWrapper'
4+
);
5+
CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test');
6+
CREATE foreign table testmulticorn (
7+
test1 character varying,
8+
test2 character varying
9+
) server multicorn_srv options (
10+
option1 'option1',
11+
test_type 'nowrite',
12+
tx_hook 'true'
13+
);
14+
insert into testmulticorn(test1, test2) VALUES ('test', 'test2');
15+
NOTICE: [('option1', 'option1'), ('test_type', 'nowrite'), ('tx_hook', 'true'), ('usermapping', 'test')]
16+
NOTICE: [('test1', 'character varying'), ('test2', 'character varying')]
17+
NOTICE: BEGIN
18+
ERROR: Error in python: NotImplementedError
19+
DETAIL: This FDW does not support the writable API
20+
NOTICE: ROLLBACK
21+
update testmulticorn set test1 = 'test';
22+
NOTICE: BEGIN
23+
NOTICE: []
24+
NOTICE: ['test1', 'test2']
25+
NOTICE: ROLLBACK
26+
ERROR: Error in python: NotImplementedError
27+
DETAIL: This FDW does not support the writable API
28+
delete from testmulticorn where test2 = 'test2 2 0';
29+
NOTICE: BEGIN
30+
NOTICE: [test2 = test2 2 0]
31+
NOTICE: ['test1', 'test2']
32+
NOTICE: ROLLBACK
33+
ERROR: Error in python: NotImplementedError
34+
DETAIL: This FDW does not support the writable API
35+
CREATE foreign table testmulticorn_write (
36+
test1 character varying,
37+
test2 character varying
38+
) server multicorn_srv options (
39+
option1 'option1',
40+
row_id_column 'test1',
41+
test_type 'returning',
42+
tx_hook 'true'
43+
);
44+
insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2');
45+
NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('tx_hook', 'true'), ('usermapping', 'test')]
46+
NOTICE: [('test1', 'character varying'), ('test2', 'character varying')]
47+
NOTICE: BEGIN
48+
NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')]
49+
NOTICE: PRECOMMIT
50+
NOTICE: COMMIT
51+
update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%';
52+
NOTICE: BEGIN
53+
NOTICE: [test1 ~~* test1 3%]
54+
NOTICE: ['test1', 'test2']
55+
NOTICE: UPDATING: test1 3 1 with [('test1', 'test'), ('test2', 'test2 1 1')]
56+
NOTICE: UPDATING: test1 3 4 with [('test1', 'test'), ('test2', 'test2 1 4')]
57+
NOTICE: UPDATING: test1 3 7 with [('test1', 'test'), ('test2', 'test2 1 7')]
58+
NOTICE: UPDATING: test1 3 10 with [('test1', 'test'), ('test2', 'test2 1 10')]
59+
NOTICE: UPDATING: test1 3 13 with [('test1', 'test'), ('test2', 'test2 1 13')]
60+
NOTICE: UPDATING: test1 3 16 with [('test1', 'test'), ('test2', 'test2 1 16')]
61+
NOTICE: UPDATING: test1 3 19 with [('test1', 'test'), ('test2', 'test2 1 19')]
62+
NOTICE: PRECOMMIT
63+
NOTICE: COMMIT
64+
delete from testmulticorn_write where test2 = 'test2 2 0';
65+
NOTICE: BEGIN
66+
NOTICE: [test2 = test2 2 0]
67+
NOTICE: ['test1', 'test2']
68+
NOTICE: DELETING: test1 1 0
69+
NOTICE: PRECOMMIT
70+
NOTICE: COMMIT
71+
-- Test returning
72+
insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2') RETURNING test1;
73+
NOTICE: BEGIN
74+
NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')]
75+
NOTICE: PRECOMMIT
76+
NOTICE: COMMIT
77+
test1
78+
----------------
79+
INSERTED: test
80+
(1 row)
81+
82+
update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%' RETURNING test1;
83+
NOTICE: BEGIN
84+
NOTICE: [test1 ~~* test1 3%]
85+
NOTICE: ['test1', 'test2']
86+
NOTICE: UPDATING: test1 3 1 with [('test1', 'test'), ('test2', 'test2 1 1')]
87+
NOTICE: UPDATING: test1 3 4 with [('test1', 'test'), ('test2', 'test2 1 4')]
88+
NOTICE: UPDATING: test1 3 7 with [('test1', 'test'), ('test2', 'test2 1 7')]
89+
NOTICE: UPDATING: test1 3 10 with [('test1', 'test'), ('test2', 'test2 1 10')]
90+
NOTICE: UPDATING: test1 3 13 with [('test1', 'test'), ('test2', 'test2 1 13')]
91+
NOTICE: UPDATING: test1 3 16 with [('test1', 'test'), ('test2', 'test2 1 16')]
92+
NOTICE: UPDATING: test1 3 19 with [('test1', 'test'), ('test2', 'test2 1 19')]
93+
NOTICE: PRECOMMIT
94+
NOTICE: COMMIT
95+
test1
96+
---------------
97+
UPDATED: test
98+
UPDATED: test
99+
UPDATED: test
100+
UPDATED: test
101+
UPDATED: test
102+
UPDATED: test
103+
UPDATED: test
104+
(7 rows)
105+
106+
delete from testmulticorn_write where test1 = 'test1 1 0' returning test2, test1;
107+
NOTICE: BEGIN
108+
NOTICE: [test1 = test1 1 0]
109+
NOTICE: ['test1', 'test2']
110+
NOTICE: DELETING: test1 1 0
111+
NOTICE: PRECOMMIT
112+
NOTICE: COMMIT
113+
test2 | test1
114+
-----------+-----------
115+
test2 2 0 | test1 1 0
116+
(1 row)
117+
118+
DROP foreign table testmulticorn_write;
119+
-- Now test with another column
120+
CREATE foreign table testmulticorn_write(
121+
test1 character varying,
122+
test2 character varying
123+
) server multicorn_srv options (
124+
option1 'option1',
125+
row_id_column 'test2'
126+
);
127+
insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2');
128+
NOTICE: [('option1', 'option1'), ('row_id_column', 'test2'), ('usermapping', 'test')]
129+
NOTICE: [('test1', 'character varying'), ('test2', 'character varying')]
130+
NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')]
131+
update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%';
132+
NOTICE: [test1 ~~* test1 3%]
133+
NOTICE: ['test1', 'test2']
134+
NOTICE: UPDATING: test2 1 1 with [('test1', 'test'), ('test2', 'test2 1 1')]
135+
NOTICE: UPDATING: test2 1 4 with [('test1', 'test'), ('test2', 'test2 1 4')]
136+
NOTICE: UPDATING: test2 1 7 with [('test1', 'test'), ('test2', 'test2 1 7')]
137+
NOTICE: UPDATING: test2 1 10 with [('test1', 'test'), ('test2', 'test2 1 10')]
138+
NOTICE: UPDATING: test2 1 13 with [('test1', 'test'), ('test2', 'test2 1 13')]
139+
NOTICE: UPDATING: test2 1 16 with [('test1', 'test'), ('test2', 'test2 1 16')]
140+
NOTICE: UPDATING: test2 1 19 with [('test1', 'test'), ('test2', 'test2 1 19')]
141+
delete from testmulticorn_write where test2 = 'test2 2 0';
142+
NOTICE: [test2 = test2 2 0]
143+
NOTICE: ['test2']
144+
NOTICE: DELETING: test2 2 0
145+
update testmulticorn_write set test2 = 'test' where test2 = 'test2 1 1';
146+
NOTICE: [test2 = test2 1 1]
147+
NOTICE: ['test1', 'test2']
148+
NOTICE: UPDATING: test2 1 1 with [('test1', 'test1 3 1'), ('test2', 'test')]
149+
DROP foreign table testmulticorn_write;
150+
-- Now test with other types
151+
CREATE foreign table testmulticorn_write(
152+
test1 date,
153+
test2 timestamp
154+
) server multicorn_srv options (
155+
option1 'option1',
156+
row_id_column 'test2',
157+
test_type 'date'
158+
);
159+
insert into testmulticorn_write(test1, test2) VALUES ('2012-01-01', '2012-01-01 00:00:00');
160+
NOTICE: [('option1', 'option1'), ('row_id_column', 'test2'), ('test_type', 'date'), ('usermapping', 'test')]
161+
NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')]
162+
NOTICE: INSERTING: [('test1', datetime.date(2012, 1, 1)), ('test2', datetime.datetime(2012, 1, 1, 0, 0))]
163+
delete from testmulticorn_write where test2 > '2011-12-03';
164+
NOTICE: [test2 > 2011-12-03 00:00:00]
165+
NOTICE: ['test2']
166+
NOTICE: DELETING: 2011-12-03 14:30:25
167+
update testmulticorn_write set test1 = date_trunc('day', test1) where test2 = '2011-09-03 14:30:25';
168+
NOTICE: [test2 = 2011-09-03 14:30:25]
169+
NOTICE: ['test1', 'test2']
170+
NOTICE: UPDATING: 2011-09-03 14:30:25 with [('test1', datetime.date(2011, 9, 2)), ('test2', datetime.datetime(2011, 9, 3, 14, 30, 25))]
171+
DROP foreign table testmulticorn_write;
172+
-- Test with unknown column
173+
CREATE foreign table testmulticorn_write(
174+
test1 date,
175+
test2 timestamp
176+
) server multicorn_srv options (
177+
option1 'option1',
178+
row_id_column 'teststuff',
179+
test_type 'date'
180+
);
181+
delete from testmulticorn_write;
182+
NOTICE: [('option1', 'option1'), ('row_id_column', 'teststuff'), ('test_type', 'date'), ('usermapping', 'test')]
183+
NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')]
184+
ERROR: The rowid attribute does not exist
185+
DROP USER MAPPING FOR current_user SERVER multicorn_srv;
186+
DROP EXTENSION multicorn cascade;
187+
NOTICE: drop cascades to 3 other objects
188+
DETAIL: drop cascades to server multicorn_srv
189+
drop cascades to foreign table testmulticorn
190+
drop cascades to foreign table testmulticorn_write

0 commit comments

Comments
 (0)