pglib 5.12.0__tar.gz → 5.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {pglib-5.12.0/pglib.egg-info → pglib-5.13.0}/PKG-INFO +4 -6
  2. {pglib-5.12.0 → pglib-5.13.0/pglib.egg-info}/PKG-INFO +4 -6
  3. {pglib-5.12.0 → pglib-5.13.0}/pyproject.toml +2 -3
  4. {pglib-5.12.0 → pglib-5.13.0}/src/connection.cpp +25 -19
  5. {pglib-5.12.0 → pglib-5.13.0}/src/errors.cpp +9 -2
  6. {pglib-5.12.0 → pglib-5.13.0}/test/test_sync.py +45 -4
  7. {pglib-5.12.0 → pglib-5.13.0}/LICENSE +0 -0
  8. {pglib-5.12.0 → pglib-5.13.0}/MANIFEST.in +0 -0
  9. {pglib-5.12.0 → pglib-5.13.0}/README.rst +0 -0
  10. {pglib-5.12.0 → pglib-5.13.0}/pglib/__init__.py +0 -0
  11. {pglib-5.12.0 → pglib-5.13.0}/pglib/asyncpglib.py +0 -0
  12. {pglib-5.12.0 → pglib-5.13.0}/pglib.egg-info/SOURCES.txt +0 -0
  13. {pglib-5.12.0 → pglib-5.13.0}/pglib.egg-info/dependency_links.txt +0 -0
  14. {pglib-5.12.0 → pglib-5.13.0}/pglib.egg-info/requires.txt +0 -0
  15. {pglib-5.12.0 → pglib-5.13.0}/pglib.egg-info/top_level.txt +0 -0
  16. {pglib-5.12.0 → pglib-5.13.0}/setup.cfg +0 -0
  17. {pglib-5.12.0 → pglib-5.13.0}/setup.py +0 -0
  18. {pglib-5.12.0 → pglib-5.13.0}/src/byteswap.h +0 -0
  19. {pglib-5.12.0 → pglib-5.13.0}/src/connection.h +0 -0
  20. {pglib-5.12.0 → pglib-5.13.0}/src/conninfoopt.cpp +0 -0
  21. {pglib-5.12.0 → pglib-5.13.0}/src/conninfoopt.h +0 -0
  22. {pglib-5.12.0 → pglib-5.13.0}/src/datatypes.cpp +0 -0
  23. {pglib-5.12.0 → pglib-5.13.0}/src/datatypes.h +0 -0
  24. {pglib-5.12.0 → pglib-5.13.0}/src/debug.cpp +0 -0
  25. {pglib-5.12.0 → pglib-5.13.0}/src/debug.h +0 -0
  26. {pglib-5.12.0 → pglib-5.13.0}/src/enums.cpp +0 -0
  27. {pglib-5.12.0 → pglib-5.13.0}/src/enums.h +0 -0
  28. {pglib-5.12.0 → pglib-5.13.0}/src/errors.h +0 -0
  29. {pglib-5.12.0 → pglib-5.13.0}/src/getdata.cpp +0 -0
  30. {pglib-5.12.0 → pglib-5.13.0}/src/getdata.h +0 -0
  31. {pglib-5.12.0 → pglib-5.13.0}/src/juliandate.cpp +0 -0
  32. {pglib-5.12.0 → pglib-5.13.0}/src/juliandate.h +0 -0
  33. {pglib-5.12.0 → pglib-5.13.0}/src/params.cpp +0 -0
  34. {pglib-5.12.0 → pglib-5.13.0}/src/params.h +0 -0
  35. {pglib-5.12.0 → pglib-5.13.0}/src/pgarrays.cpp +0 -0
  36. {pglib-5.12.0 → pglib-5.13.0}/src/pgarrays.h +0 -0
  37. {pglib-5.12.0 → pglib-5.13.0}/src/pglib.cpp +0 -0
  38. {pglib-5.12.0 → pglib-5.13.0}/src/pglib.h +0 -0
  39. {pglib-5.12.0 → pglib-5.13.0}/src/pgtypes.h +0 -0
  40. {pglib-5.12.0 → pglib-5.13.0}/src/resultset.cpp +0 -0
  41. {pglib-5.12.0 → pglib-5.13.0}/src/resultset.h +0 -0
  42. {pglib-5.12.0 → pglib-5.13.0}/src/row.cpp +0 -0
  43. {pglib-5.12.0 → pglib-5.13.0}/src/row.h +0 -0
  44. {pglib-5.12.0 → pglib-5.13.0}/src/runtime.cpp +0 -0
  45. {pglib-5.12.0 → pglib-5.13.0}/src/runtime.h +0 -0
  46. {pglib-5.12.0 → pglib-5.13.0}/src/type_hstore.cpp +0 -0
  47. {pglib-5.12.0 → pglib-5.13.0}/src/type_hstore.h +0 -0
  48. {pglib-5.12.0 → pglib-5.13.0}/src/type_json.cpp +0 -0
  49. {pglib-5.12.0 → pglib-5.13.0}/src/type_json.h +0 -0
  50. {pglib-5.12.0 → pglib-5.13.0}/src/type_ltree.cpp +0 -0
  51. {pglib-5.12.0 → pglib-5.13.0}/src/type_ltree.h +0 -0
  52. {pglib-5.12.0 → pglib-5.13.0}/test/test_async.py +0 -0
  53. {pglib-5.12.0 → pglib-5.13.0}/test/testutils.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pglib
3
- Version: 5.12.0
3
+ Version: 5.13.0
4
4
  Summary: A PostgreSQL interface
5
5
  Author-email: Michael Kleehammer <michael@kleehammer.com>
6
- License-Expression: MIT
6
+ License: MIT
7
7
  Project-URL: Homepage, https://gitlab.com/mkleehammer/pglib
8
8
  Project-URL: Repository, https://gitlab.com/mkleehammer/pglib
9
9
  Keywords: postgresql,postgres
@@ -17,10 +17,8 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.14
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/x-rst
20
- License-File: LICENSE
21
20
  Provides-Extra: test
22
- Requires-Dist: pytest; extra == "test"
23
- Dynamic: license-file
21
+ License-File: LICENSE
24
22
 
25
23
 
26
24
  pglib is a Python 3.10+ module for working with PostgreSQL databases. It is a C extension that
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pglib
3
- Version: 5.12.0
3
+ Version: 5.13.0
4
4
  Summary: A PostgreSQL interface
5
5
  Author-email: Michael Kleehammer <michael@kleehammer.com>
6
- License-Expression: MIT
6
+ License: MIT
7
7
  Project-URL: Homepage, https://gitlab.com/mkleehammer/pglib
8
8
  Project-URL: Repository, https://gitlab.com/mkleehammer/pglib
9
9
  Keywords: postgresql,postgres
@@ -17,10 +17,8 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.14
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/x-rst
20
- License-File: LICENSE
21
20
  Provides-Extra: test
22
- Requires-Dist: pytest; extra == "test"
23
- Dynamic: license-file
21
+ License-File: LICENSE
24
22
 
25
23
 
26
24
  pglib is a Python 3.10+ module for working with PostgreSQL databases. It is a C extension that
@@ -4,11 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pglib"
7
- version = "5.12.0"
7
+ version = "5.13.0"
8
8
  description = "A PostgreSQL interface"
9
9
  readme = "README.rst"
10
- license = "MIT"
11
- license-files = ["LICENSE"]
10
+ license = {text = "MIT"}
12
11
  requires-python = ">=3.10"
13
12
  authors = [
14
13
  {name = "Michael Kleehammer", email = "michael@kleehammer.com"}
@@ -170,8 +170,8 @@ static PGresult* internal_execute(PyObject* self, PyObject* args)
170
170
 
171
171
  if (result == 0)
172
172
  {
173
- // Apparently this only happens for very serious errors, but the docs aren't terribly clear.
174
- PyErr_SetString(Error, "Fatal error");
173
+ // This happens for out-of-memory or inability to send the command to the server.
174
+ SetConnectionError(cnxn);
175
175
  return 0;
176
176
  }
177
177
 
@@ -197,8 +197,8 @@ static PyObject* Connection_script(PyObject* self, PyObject* args)
197
197
  return 0;
198
198
 
199
199
  ResultHolder result = PQexec(cnxn->pgconn, szScript);
200
- if (result == 0)
201
- return 0;
200
+ if (result.p == 0)
201
+ return SetConnectionError(cnxn);
202
202
 
203
203
  switch (PQresultStatus(result)) {
204
204
  case PGRES_BAD_RESPONSE:
@@ -271,7 +271,7 @@ static PyObject* Connection_copy_from(PyObject* self, PyObject* args)
271
271
  Py_END_ALLOW_THREADS
272
272
 
273
273
  if (result == 0)
274
- return 0;
274
+ return SetConnectionError(cnxn);
275
275
 
276
276
  switch (PQresultStatus(result)) {
277
277
  case PGRES_COPY_IN:
@@ -407,7 +407,7 @@ static PyObject* Connection_copy_to(PyObject* self, PyObject* args)
407
407
  Py_END_ALLOW_THREADS
408
408
 
409
409
  if (result == 0)
410
- return 0;
410
+ return SetConnectionError(cnxn);
411
411
 
412
412
  switch (PQresultStatus(result)) {
413
413
  case PGRES_COPY_OUT:
@@ -432,7 +432,7 @@ static PyObject* Connection_copy_to(PyObject* self, PyObject* args)
432
432
  Py_END_ALLOW_THREADS
433
433
 
434
434
  if (cb == -2) {
435
- return SetResultError(result.Detach());
435
+ return SetConnectionError(cnxn);
436
436
  }
437
437
 
438
438
  if (cb == -1) {
@@ -461,7 +461,6 @@ static PyObject* Connection_copy_to(PyObject* self, PyObject* args)
461
461
  Py_END_ALLOW_THREADS
462
462
 
463
463
  if (status != PGRES_COMMAND_OK) {
464
- // SetResultError will take ownership of `result`.
465
464
  return SetResultError(final_result.Detach());
466
465
  }
467
466
 
@@ -542,7 +541,7 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
542
541
  Py_END_ALLOW_THREADS
543
542
 
544
543
  if (result == 0)
545
- return 0;
544
+ return SetConnectionError(cnxn);
546
545
 
547
546
  switch (PQresultStatus(result)) {
548
547
  case PGRES_COPY_OUT:
@@ -567,7 +566,7 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
567
566
  Py_END_ALLOW_THREADS
568
567
 
569
568
  if (cb == -2) {
570
- return SetResultError(result.Detach());
569
+ return SetConnectionError(cnxn);
571
570
  }
572
571
 
573
572
  if (cb == -1) {
@@ -580,10 +579,6 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
580
579
 
581
580
  int err = PyFile_WriteString(buffer, dest);
582
581
 
583
- // while (cb > 0) {
584
- // PyObject* res = PyObject_CallObject(write_method)
585
- // }
586
-
587
582
  PQfreemem(buffer);
588
583
  if (err) {
589
584
  return 0;
@@ -600,7 +595,6 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
600
595
  Py_END_ALLOW_THREADS
601
596
 
602
597
  if (status != PGRES_COMMAND_OK) {
603
- // SetResultError will take ownership of `result`.
604
598
  return SetResultError(final_result.Detach());
605
599
  }
606
600
 
@@ -685,7 +679,7 @@ static PyObject* Connection_copy_from_csv(PyObject* self, PyObject* args, PyObje
685
679
  Py_END_ALLOW_THREADS
686
680
 
687
681
  if (result == 0)
688
- return 0;
682
+ return SetConnectionError(cnxn);
689
683
 
690
684
  switch (PQresultStatus(result)) {
691
685
  case PGRES_COPY_IN:
@@ -1048,13 +1042,17 @@ static PyObject* Connection_begin(PyObject* self, PyObject* args)
1048
1042
  if (txnstatus == PQTRANS_IDLE)
1049
1043
  {
1050
1044
  result = PQexec(cnxn->pgconn, "BEGIN");
1051
- status = PQresultStatus(result);
1045
+ if (result)
1046
+ status = PQresultStatus(result);
1052
1047
  }
1053
1048
  Py_END_ALLOW_THREADS
1054
1049
 
1055
1050
  if (txnstatus != PQTRANS_IDLE)
1056
1051
  return PyErr_Format(Error, "Connection transaction status is not idle: %s", NameFromTxnFlag(txnstatus));
1057
1052
 
1053
+ if (result == 0)
1054
+ return SetConnectionError(cnxn);
1055
+
1058
1056
  if (status != PGRES_COMMAND_OK)
1059
1057
  return SetResultError(result);
1060
1058
 
@@ -1081,13 +1079,17 @@ static PyObject* Connection_commit(PyObject* self, PyObject* args)
1081
1079
  if (txnstatus == PQTRANS_INTRANS)
1082
1080
  {
1083
1081
  result = PQexec(cnxn->pgconn, "COMMIT");
1084
- status = PQresultStatus(result);
1082
+ if (result)
1083
+ status = PQresultStatus(result);
1085
1084
  }
1086
1085
  Py_END_ALLOW_THREADS
1087
1086
 
1088
1087
  if (txnstatus != PQTRANS_IDLE && txnstatus != PQTRANS_INTRANS)
1089
1088
  return PyErr_Format(Error, "Connection transaction status is invalid: %s", NameFromTxnFlag(txnstatus));
1090
1089
 
1090
+ if (txnstatus == PQTRANS_INTRANS && result == 0)
1091
+ return SetConnectionError(cnxn);
1092
+
1091
1093
  if (status != PGRES_COMMAND_OK)
1092
1094
  return SetResultError(result);
1093
1095
 
@@ -1110,9 +1112,13 @@ static PyObject* Connection_rollback(PyObject* self, PyObject* args)
1110
1112
 
1111
1113
  Py_BEGIN_ALLOW_THREADS
1112
1114
  result = PQexec(cnxn->pgconn, "ROLLBACK");
1113
- status = PQresultStatus(result);
1115
+ if (result)
1116
+ status = PQresultStatus(result);
1114
1117
  Py_END_ALLOW_THREADS
1115
1118
 
1119
+ if (result == 0)
1120
+ return SetConnectionError(cnxn);
1121
+
1116
1122
  if (status != PGRES_COMMAND_OK)
1117
1123
  return SetResultError(result);
1118
1124
 
@@ -29,6 +29,8 @@ static const ResultErrorField errorFields[] =
29
29
  PyObject* SetConnectionError(PGconn* pgconn)
30
30
  {
31
31
  const char* szMessage = PQerrorMessage(pgconn);
32
+ if (szMessage == 0 || szMessage[0] == 0)
33
+ szMessage = "connection error (no details available)";
32
34
  PyErr_SetString(Error, szMessage);
33
35
  return 0;
34
36
  }
@@ -63,8 +65,12 @@ PyObject* SetResultError(PGresult* r)
63
65
 
64
66
  const char* szMessage = PQresultErrorMessage(result);
65
67
  const char* szSQLSTATE = PQresultErrorField(result, PG_DIAG_SQLSTATE);
66
- if (!szMessage || !szSQLSTATE)
67
- return PyErr_NoMemory();
68
+
69
+ // Handle cases where error info isn't available (e.g., connection terminated)
70
+ if (!szMessage || szMessage[0] == 0)
71
+ szMessage = "unknown error";
72
+ if (!szSQLSTATE)
73
+ szSQLSTATE = "?????"; // Unknown SQLSTATE
68
74
 
69
75
  Object msg(PyUnicode_FromFormat("[%s] %s", szSQLSTATE, szMessage));
70
76
  if (!msg)
@@ -96,6 +102,7 @@ PyObject* SetResultError(PGresult* r)
96
102
  }
97
103
 
98
104
  PyErr_SetObject(Error, error);
105
+ Py_DECREF(error);
99
106
 
100
107
  return 0;
101
108
  }
@@ -523,16 +523,20 @@ def test_money(cnxn):
523
523
  # parses the values we pass as strings before we even get to pglib.
524
524
 
525
525
 
526
- @pytest.mark.skip('LC_ALL locale does not exist')
527
526
  def test_decimal_german(cnxn):
528
- locale.setlocale(locale.LC_ALL, 'de_DE')
527
+ try:
528
+ locale.setlocale(locale.LC_ALL, 'de_DE.utf8')
529
+ except locale.Error:
530
+ pytest.skip('de_DE.utf8 locale not available')
529
531
  values = [Decimal(s) for s in ['-3.0000000', '123456.7890']]
530
532
  _test_type(cnxn, 'decimal(100,7)', values)
531
533
 
532
534
 
533
- @pytest.mark.skip('LC_ALL locale does not exist')
534
535
  def test_money_german(cnxn):
535
- locale.setlocale(locale.LC_ALL, 'de_DE')
536
+ try:
537
+ locale.setlocale(locale.LC_ALL, 'de_DE.utf8')
538
+ except locale.Error:
539
+ pytest.skip('de_DE.utf8 locale not available')
536
540
  values = [Decimal(s) for s in ['1.23', '0.0', '123.45', '-12.34']]
537
541
  _test_type(cnxn, 'money', values, check_comparison=False)
538
542
 
@@ -1308,3 +1312,40 @@ def test_large_insert(cnxn):
1308
1312
  query = (Path(__file__).parent / 'large-insert.sql').read_text(encoding='utf8')
1309
1313
  print('-' * 40)
1310
1314
  cnxn.execute(query)
1315
+
1316
+
1317
+ def test_terminated_connection_error_message():
1318
+ """
1319
+ Verify that when a connection is terminated, we get a meaningful error
1320
+ message from libpq instead of a generic "Fatal error".
1321
+
1322
+ This tests the fix for the case where PQexecParams returns NULL.
1323
+ """
1324
+ # Create the connection we'll terminate
1325
+ target = pglib.connect(CONNINFO)
1326
+ target_pid = target.pid
1327
+
1328
+ # Use a separate connection to terminate the target
1329
+ killer = pglib.connect(CONNINFO)
1330
+ killer.execute("select pg_terminate_backend($1)", target_pid)
1331
+ killer.close()
1332
+
1333
+ # Give PostgreSQL a moment to process the termination
1334
+ sleep(0.1)
1335
+
1336
+ # Now try to execute on the terminated connection
1337
+ with pytest.raises(pglib.Error) as exc_info:
1338
+ target.execute("select 1")
1339
+
1340
+ # The error message should NOT be the generic "Fatal error"
1341
+ # It should contain something meaningful from libpq
1342
+ error_msg = str(exc_info.value)
1343
+ assert "Fatal error" not in error_msg, \
1344
+ f"Expected meaningful error message, got generic 'Fatal error': {error_msg}"
1345
+
1346
+ # Should contain some indication of connection issue
1347
+ assert any(phrase in error_msg.lower() for phrase in
1348
+ ["terminat", "connection", "server", "closed"]), \
1349
+ f"Error message doesn't indicate connection problem: {error_msg}"
1350
+
1351
+ target.close()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes