pglib 5.11.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.11.0/pglib.egg-info → pglib-5.13.0}/PKG-INFO +4 -6
  2. {pglib-5.11.0 → pglib-5.13.0/pglib.egg-info}/PKG-INFO +4 -6
  3. {pglib-5.11.0 → pglib-5.13.0}/pyproject.toml +2 -3
  4. {pglib-5.11.0 → pglib-5.13.0}/src/connection.cpp +130 -16
  5. {pglib-5.11.0 → pglib-5.13.0}/src/errors.cpp +9 -2
  6. {pglib-5.11.0 → pglib-5.13.0}/test/test_sync.py +78 -4
  7. {pglib-5.11.0 → pglib-5.13.0}/LICENSE +0 -0
  8. {pglib-5.11.0 → pglib-5.13.0}/MANIFEST.in +0 -0
  9. {pglib-5.11.0 → pglib-5.13.0}/README.rst +0 -0
  10. {pglib-5.11.0 → pglib-5.13.0}/pglib/__init__.py +0 -0
  11. {pglib-5.11.0 → pglib-5.13.0}/pglib/asyncpglib.py +0 -0
  12. {pglib-5.11.0 → pglib-5.13.0}/pglib.egg-info/SOURCES.txt +0 -0
  13. {pglib-5.11.0 → pglib-5.13.0}/pglib.egg-info/dependency_links.txt +0 -0
  14. {pglib-5.11.0 → pglib-5.13.0}/pglib.egg-info/requires.txt +0 -0
  15. {pglib-5.11.0 → pglib-5.13.0}/pglib.egg-info/top_level.txt +0 -0
  16. {pglib-5.11.0 → pglib-5.13.0}/setup.cfg +0 -0
  17. {pglib-5.11.0 → pglib-5.13.0}/setup.py +0 -0
  18. {pglib-5.11.0 → pglib-5.13.0}/src/byteswap.h +0 -0
  19. {pglib-5.11.0 → pglib-5.13.0}/src/connection.h +0 -0
  20. {pglib-5.11.0 → pglib-5.13.0}/src/conninfoopt.cpp +0 -0
  21. {pglib-5.11.0 → pglib-5.13.0}/src/conninfoopt.h +0 -0
  22. {pglib-5.11.0 → pglib-5.13.0}/src/datatypes.cpp +0 -0
  23. {pglib-5.11.0 → pglib-5.13.0}/src/datatypes.h +0 -0
  24. {pglib-5.11.0 → pglib-5.13.0}/src/debug.cpp +0 -0
  25. {pglib-5.11.0 → pglib-5.13.0}/src/debug.h +0 -0
  26. {pglib-5.11.0 → pglib-5.13.0}/src/enums.cpp +0 -0
  27. {pglib-5.11.0 → pglib-5.13.0}/src/enums.h +0 -0
  28. {pglib-5.11.0 → pglib-5.13.0}/src/errors.h +0 -0
  29. {pglib-5.11.0 → pglib-5.13.0}/src/getdata.cpp +0 -0
  30. {pglib-5.11.0 → pglib-5.13.0}/src/getdata.h +0 -0
  31. {pglib-5.11.0 → pglib-5.13.0}/src/juliandate.cpp +0 -0
  32. {pglib-5.11.0 → pglib-5.13.0}/src/juliandate.h +0 -0
  33. {pglib-5.11.0 → pglib-5.13.0}/src/params.cpp +0 -0
  34. {pglib-5.11.0 → pglib-5.13.0}/src/params.h +0 -0
  35. {pglib-5.11.0 → pglib-5.13.0}/src/pgarrays.cpp +0 -0
  36. {pglib-5.11.0 → pglib-5.13.0}/src/pgarrays.h +0 -0
  37. {pglib-5.11.0 → pglib-5.13.0}/src/pglib.cpp +0 -0
  38. {pglib-5.11.0 → pglib-5.13.0}/src/pglib.h +0 -0
  39. {pglib-5.11.0 → pglib-5.13.0}/src/pgtypes.h +0 -0
  40. {pglib-5.11.0 → pglib-5.13.0}/src/resultset.cpp +0 -0
  41. {pglib-5.11.0 → pglib-5.13.0}/src/resultset.h +0 -0
  42. {pglib-5.11.0 → pglib-5.13.0}/src/row.cpp +0 -0
  43. {pglib-5.11.0 → pglib-5.13.0}/src/row.h +0 -0
  44. {pglib-5.11.0 → pglib-5.13.0}/src/runtime.cpp +0 -0
  45. {pglib-5.11.0 → pglib-5.13.0}/src/runtime.h +0 -0
  46. {pglib-5.11.0 → pglib-5.13.0}/src/type_hstore.cpp +0 -0
  47. {pglib-5.11.0 → pglib-5.13.0}/src/type_hstore.h +0 -0
  48. {pglib-5.11.0 → pglib-5.13.0}/src/type_json.cpp +0 -0
  49. {pglib-5.11.0 → pglib-5.13.0}/src/type_json.h +0 -0
  50. {pglib-5.11.0 → pglib-5.13.0}/src/type_ltree.cpp +0 -0
  51. {pglib-5.11.0 → pglib-5.13.0}/src/type_ltree.h +0 -0
  52. {pglib-5.11.0 → pglib-5.13.0}/test/test_async.py +0 -0
  53. {pglib-5.11.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.11.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.11.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.11.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:
@@ -365,6 +365,112 @@ static PyObject* Connection_copy_from(PyObject* self, PyObject* args)
365
365
  }
366
366
 
367
367
 
368
+ const char* doc_copy_to =
369
+ "Connection.copy_to(command, dest) --> int\n"
370
+ "\n"
371
+ "Executes the given COPY TO command and returns the number of records copied.\n"
372
+ "\n"
373
+ "command\n"
374
+ " The copy command which must be 'to stdout'.\n"
375
+ "\n"
376
+ "dest\n"
377
+ " The file-like object to write to. Strings will be written, not bytes, so\n"
378
+ " open in text mode.\n"
379
+ "\n"
380
+ "Examples:\n"
381
+ " cnxn.copy_to('copy t1 to stdout csv header', open('test.csv', 'w'))\n"
382
+ " cnxn.copy_to('copy t1(a, b, c) to stdout csv header', open('test.csv', 'w'))\n"
383
+ " cnxn.copy_to('copy (select * from t1 where id > 10) to stdout csv', f)\n";
384
+
385
+ static PyObject* Connection_copy_to(PyObject* self, PyObject* args)
386
+ {
387
+ PyObject* command;
388
+ PyObject* dest;
389
+ if (!PyArg_ParseTuple(args, "UO", &command, &dest))
390
+ return 0;
391
+
392
+ Connection* cnxn = CastConnection(self, REQUIRE_OPEN);
393
+ if (!cnxn)
394
+ return 0;
395
+
396
+ if (!PyObject_HasAttrString(dest, "write"))
397
+ return PyErr_Format(Error, "Destination must be a file-like object.");
398
+ Object write_method(PyObject_GetAttrString(dest, "write"));
399
+
400
+ const char* szSQL = PyUnicode_AsUTF8(command);
401
+ if (!szSQL)
402
+ return 0;
403
+
404
+ ResultHolder result;
405
+ Py_BEGIN_ALLOW_THREADS
406
+ result = PQexec(cnxn->pgconn, szSQL);
407
+ Py_END_ALLOW_THREADS
408
+
409
+ if (result == 0)
410
+ return SetConnectionError(cnxn);
411
+
412
+ switch (PQresultStatus(result)) {
413
+ case PGRES_COPY_OUT:
414
+ // This is what we are expecting.
415
+ break;
416
+
417
+ case PGRES_BAD_RESPONSE:
418
+ case PGRES_NONFATAL_ERROR:
419
+ case PGRES_FATAL_ERROR:
420
+ return SetResultError(result.Detach());
421
+
422
+ default:
423
+ return PyErr_Format(Error, "Result was not PGRES_COPY_OUT: %d", (int)PQresultStatus(result));
424
+ }
425
+
426
+
427
+ for (;;) {
428
+ int cb = 0;
429
+ char* buffer;
430
+ Py_BEGIN_ALLOW_THREADS
431
+ cb = PQgetCopyData(cnxn->pgconn, &buffer, 0);
432
+ Py_END_ALLOW_THREADS
433
+
434
+ if (cb == -2) {
435
+ return SetConnectionError(cnxn);
436
+ }
437
+
438
+ if (cb == -1) {
439
+ // The copy is complete.
440
+ break;
441
+ }
442
+
443
+ // We have a buffer of byte data. We have the length (`cb`), but the libpq docs say
444
+ // that the string is also zero terminated, so we're going to try not calling 'write'.
445
+
446
+ int err = PyFile_WriteString(buffer, dest);
447
+
448
+ PQfreemem(buffer);
449
+ if (err) {
450
+ return 0;
451
+ }
452
+ }
453
+
454
+ // After a copy, you have to get another result to know if it was successful.
455
+
456
+ ResultHolder final_result;
457
+ ExecStatusType status = PGRES_COMMAND_OK;
458
+ Py_BEGIN_ALLOW_THREADS
459
+ final_result = PQgetResult(cnxn->pgconn);
460
+ status = PQresultStatus(final_result);
461
+ Py_END_ALLOW_THREADS
462
+
463
+ if (status != PGRES_COMMAND_OK) {
464
+ return SetResultError(final_result.Detach());
465
+ }
466
+
467
+ const char* sz = PQcmdTuples(final_result);
468
+ if (sz == 0 || *sz == 0)
469
+ Py_RETURN_NONE;
470
+ return PyLong_FromLong(atoi(sz));
471
+ }
472
+
473
+
368
474
  const char* doc_copy_to_csv =
369
475
  "Connection.copy_to_csv(table, dest, header=0, delimiter=',', quote='\"')\n"
370
476
  "\n"
@@ -435,7 +541,7 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
435
541
  Py_END_ALLOW_THREADS
436
542
 
437
543
  if (result == 0)
438
- return 0;
544
+ return SetConnectionError(cnxn);
439
545
 
440
546
  switch (PQresultStatus(result)) {
441
547
  case PGRES_COPY_OUT:
@@ -460,7 +566,7 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
460
566
  Py_END_ALLOW_THREADS
461
567
 
462
568
  if (cb == -2) {
463
- return SetResultError(result.Detach());
569
+ return SetConnectionError(cnxn);
464
570
  }
465
571
 
466
572
  if (cb == -1) {
@@ -473,10 +579,6 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
473
579
 
474
580
  int err = PyFile_WriteString(buffer, dest);
475
581
 
476
- // while (cb > 0) {
477
- // PyObject* res = PyObject_CallObject(write_method)
478
- // }
479
-
480
582
  PQfreemem(buffer);
481
583
  if (err) {
482
584
  return 0;
@@ -493,7 +595,6 @@ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject
493
595
  Py_END_ALLOW_THREADS
494
596
 
495
597
  if (status != PGRES_COMMAND_OK) {
496
- // SetResultError will take ownership of `result`.
497
598
  return SetResultError(final_result.Detach());
498
599
  }
499
600
 
@@ -578,7 +679,7 @@ static PyObject* Connection_copy_from_csv(PyObject* self, PyObject* args, PyObje
578
679
  Py_END_ALLOW_THREADS
579
680
 
580
681
  if (result == 0)
581
- return 0;
682
+ return SetConnectionError(cnxn);
582
683
 
583
684
  switch (PQresultStatus(result)) {
584
685
  case PGRES_COPY_IN:
@@ -941,13 +1042,17 @@ static PyObject* Connection_begin(PyObject* self, PyObject* args)
941
1042
  if (txnstatus == PQTRANS_IDLE)
942
1043
  {
943
1044
  result = PQexec(cnxn->pgconn, "BEGIN");
944
- status = PQresultStatus(result);
1045
+ if (result)
1046
+ status = PQresultStatus(result);
945
1047
  }
946
1048
  Py_END_ALLOW_THREADS
947
1049
 
948
1050
  if (txnstatus != PQTRANS_IDLE)
949
1051
  return PyErr_Format(Error, "Connection transaction status is not idle: %s", NameFromTxnFlag(txnstatus));
950
1052
 
1053
+ if (result == 0)
1054
+ return SetConnectionError(cnxn);
1055
+
951
1056
  if (status != PGRES_COMMAND_OK)
952
1057
  return SetResultError(result);
953
1058
 
@@ -974,13 +1079,17 @@ static PyObject* Connection_commit(PyObject* self, PyObject* args)
974
1079
  if (txnstatus == PQTRANS_INTRANS)
975
1080
  {
976
1081
  result = PQexec(cnxn->pgconn, "COMMIT");
977
- status = PQresultStatus(result);
1082
+ if (result)
1083
+ status = PQresultStatus(result);
978
1084
  }
979
1085
  Py_END_ALLOW_THREADS
980
1086
 
981
1087
  if (txnstatus != PQTRANS_IDLE && txnstatus != PQTRANS_INTRANS)
982
1088
  return PyErr_Format(Error, "Connection transaction status is invalid: %s", NameFromTxnFlag(txnstatus));
983
1089
 
1090
+ if (txnstatus == PQTRANS_INTRANS && result == 0)
1091
+ return SetConnectionError(cnxn);
1092
+
984
1093
  if (status != PGRES_COMMAND_OK)
985
1094
  return SetResultError(result);
986
1095
 
@@ -1003,9 +1112,13 @@ static PyObject* Connection_rollback(PyObject* self, PyObject* args)
1003
1112
 
1004
1113
  Py_BEGIN_ALLOW_THREADS
1005
1114
  result = PQexec(cnxn->pgconn, "ROLLBACK");
1006
- status = PQresultStatus(result);
1115
+ if (result)
1116
+ status = PQresultStatus(result);
1007
1117
  Py_END_ALLOW_THREADS
1008
1118
 
1119
+ if (result == 0)
1120
+ return SetConnectionError(cnxn);
1121
+
1009
1122
  if (status != PGRES_COMMAND_OK)
1010
1123
  return SetResultError(result);
1011
1124
 
@@ -1631,6 +1744,7 @@ static struct PyMethodDef Connection_methods[] =
1631
1744
  { "script", Connection_script, METH_VARARGS, doc_script },
1632
1745
  { "copy_from", (PyCFunction) Connection_copy_from, METH_VARARGS | METH_KEYWORDS, doc_copy_from },
1633
1746
  { "copy_from_csv", (PyCFunction) Connection_copy_from_csv, METH_VARARGS | METH_KEYWORDS, doc_copy_from_csv },
1747
+ { "copy_to", (PyCFunction) Connection_copy_to, METH_VARARGS, doc_copy_to },
1634
1748
  { "copy_to_csv", (PyCFunction) Connection_copy_to_csv, METH_VARARGS | METH_KEYWORDS, doc_copy_to_csv},
1635
1749
  { "begin", Connection_begin, METH_NOARGS, doc_begin },
1636
1750
  { "commit", Connection_commit, METH_NOARGS, doc_commit },
@@ -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
  }
@@ -176,6 +176,39 @@ def test_copytocsv(cnxn):
176
176
  assert rows == [['a', 'b'], ['1', 'one'], ['2', 'two'], ['3', 'three']]
177
177
 
178
178
 
179
+ def test_copy_to(cnxn):
180
+ # Test copy_to with a full command, including a subquery.
181
+ cnxn.execute("create table t1(a int, b text)")
182
+ cnxn.execute("insert into t1 values (1, 'one'), (2, 'two'), (3, 'three')")
183
+
184
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf8') as tf:
185
+ # Use a subquery to only copy rows where a > 1
186
+ count = cnxn.copy_to('copy (select * from t1 where a > 1 order by a) to stdout csv header', tf)
187
+ assert count == 2
188
+
189
+ tf.flush()
190
+ with open(tf.name, mode='r', encoding='utf8') as fd:
191
+ reader = csv.reader(fd)
192
+ rows = list(reader)
193
+ assert rows == [['a', 'b'], ['2', 'two'], ['3', 'three']]
194
+
195
+
196
+ def test_copy_to_table(cnxn):
197
+ # Test copy_to with a simple table copy (no subquery).
198
+ cnxn.execute("create table t1(a int, b text)")
199
+ cnxn.execute("insert into t1 values (1, 'one'), (2, 'two')")
200
+
201
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf8') as tf:
202
+ count = cnxn.copy_to('copy t1 to stdout csv', tf)
203
+ assert count == 2
204
+
205
+ tf.flush()
206
+ with open(tf.name, mode='r', encoding='utf8') as fd:
207
+ reader = csv.reader(fd)
208
+ rows = list(reader)
209
+ assert rows == [['1', 'one'], ['2', 'two']]
210
+
211
+
179
212
  #
180
213
  # copy from
181
214
  #
@@ -490,16 +523,20 @@ def test_money(cnxn):
490
523
  # parses the values we pass as strings before we even get to pglib.
491
524
 
492
525
 
493
- @pytest.mark.skip('LC_ALL locale does not exist')
494
526
  def test_decimal_german(cnxn):
495
- 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')
496
531
  values = [Decimal(s) for s in ['-3.0000000', '123456.7890']]
497
532
  _test_type(cnxn, 'decimal(100,7)', values)
498
533
 
499
534
 
500
- @pytest.mark.skip('LC_ALL locale does not exist')
501
535
  def test_money_german(cnxn):
502
- 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')
503
540
  values = [Decimal(s) for s in ['1.23', '0.0', '123.45', '-12.34']]
504
541
  _test_type(cnxn, 'money', values, check_comparison=False)
505
542
 
@@ -1275,3 +1312,40 @@ def test_large_insert(cnxn):
1275
1312
  query = (Path(__file__).parent / 'large-insert.sql').read_text(encoding='utf8')
1276
1313
  print('-' * 40)
1277
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