From eaa98c3c27d59a3bda915e7caed60e5a76fbbc05 Mon Sep 17 00:00:00 2001
From: Pier Fumagalli <pier@usrz.com>
Date: Thu, 31 Aug 2023 23:08:39 +0200
Subject: [PATCH] Add support for notices.

---
 README.md         | 23 +++++++++++++-
 src/connection.cc | 54 ++++++++++++++++++++++++++++++--
 src/connection.h  |  1 +
 test/notices.js   | 78 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 153 insertions(+), 3 deletions(-)
 create mode 100644 test/notices.js

diff --git a/README.md b/README.md
index 53ebbb4..b7ae2d6 100644
--- a/README.md
+++ b/README.md
@@ -243,6 +243,27 @@ var msg = {
 }
 ```
 
+##### `pq.on("notify", callback:function)`
+
+Receives `DEBUG`, `LOG`, `INFO`, `NOTICE` and `WARNING` notifications.
+
+- `callback` is mandatory. It is called with an object similar to what is
+  returned by the (yet undocumented) function `pq.resultErrorFields()`.
+
+The format of the `notify` event payload is as follows:
+
+```js
+var notification = {
+  severity: 'WARNING',
+  sqlState: '01000',
+  messagePrimary: 'this is a warning message',
+  context: 'PL/pgSQL function inline_code_block line 1 at RAISE',
+  sourceFile: 'pl_exec.c',
+  sourceLine: '3917',
+  sourceFunction: 'exec_stmt_raise'
+}
+```
+
 ### COPY IN/OUT
 
 ##### `pq.putCopyData(buffer:Buffer):int`
@@ -296,7 +317,7 @@ Returns the version of the connected PostgreSQL backend server as a number.
 $ npm test
 ```
 
-To run the tests you need a PostgreSQL backend reachable by typing `psql` with no connection parameters in your terminal. The tests use [environment variables](http://www.postgresql.org/docs/9.3/static/libpq-envars.html) to connect to the backend. 
+To run the tests you need a PostgreSQL backend reachable by typing `psql` with no connection parameters in your terminal. The tests use [environment variables](http://www.postgresql.org/docs/9.3/static/libpq-envars.html) to connect to the backend.
 
 An example of supplying a specific host the tests:
 
diff --git a/src/connection.cc b/src/connection.cc
index 38d196b..c816966 100644
--- a/src/connection.cc
+++ b/src/connection.cc
@@ -302,13 +302,17 @@ NAN_METHOD(Connection::ResultErrorMessage) {
   info.GetReturnValue().Set(Nan::New<v8::String>(status).ToLocalChecked());
 }
 
-# define SET_E(key, name) \
-  field = PQresultErrorField(self->lastResult, key); \
+// set error field from the pg_result "lastResult" parameter
+# define SET_ER(key, name, lastResult) \
+  field = PQresultErrorField(lastResult, key); \
   if(field != NULL) { \
     Nan::Set(result, \
         Nan::New(name).ToLocalChecked(), Nan::New(field).ToLocalChecked()); \
   }
 
+// set error field using the default "self->lastResult" pg_result
+# define SET_E(key, name) SET_ER(key, name, self->lastResult)
+
 NAN_METHOD(Connection::ResultErrorFields) {
   Connection *self = NODE_THIS();
 
@@ -672,6 +676,8 @@ bool Connection::ConnectDB(const char* paramString) {
   TRACEF("Connection::ConnectDB:Connection parameters: %s\n", paramString);
   this->pq = PQconnectdb(paramString);
 
+  PQsetNoticeReceiver(this->pq, notice_receiver, this);
+
   ConnStatusType status = PQstatus(this->pq);
 
   if(status != CONNECTION_OK) {
@@ -696,6 +702,50 @@ char * Connection::ErrorMessage() {
   return PQerrorMessage(this->pq);
 }
 
+void Connection::notice_receiver(void* connection, const pg_result* noticeResult) {
+  LOG("Connection::notice_received");
+
+  Connection* self = (Connection*) connection;
+  Nan::HandleScope scope;
+
+  v8::Local<v8::Object> result = Nan::New<v8::Object>();
+  char* field;
+  SET_ER(PG_DIAG_SEVERITY, "severity", noticeResult);
+  SET_ER(PG_DIAG_SQLSTATE, "sqlState", noticeResult);
+  SET_ER(PG_DIAG_MESSAGE_PRIMARY, "messagePrimary", noticeResult);
+  SET_ER(PG_DIAG_MESSAGE_DETAIL, "messageDetail", noticeResult);
+  SET_ER(PG_DIAG_MESSAGE_HINT, "messageHint", noticeResult);
+  SET_ER(PG_DIAG_STATEMENT_POSITION, "statementPosition", noticeResult);
+  SET_ER(PG_DIAG_INTERNAL_POSITION, "internalPosition", noticeResult);
+  SET_ER(PG_DIAG_INTERNAL_QUERY, "internalQuery", noticeResult);
+  SET_ER(PG_DIAG_CONTEXT, "context", noticeResult);
+#ifdef MORE_ERROR_FIELDS_SUPPORTED
+  SET_ER(PG_DIAG_SCHEMA_NAME, "schemaName", noticeResult);
+  SET_ER(PG_DIAG_TABLE_NAME, "tableName", noticeResult);
+  SET_ER(PG_DIAG_COLUMN_NAME, "columnName", noticeResult);
+  SET_ER(PG_DIAG_DATATYPE_NAME, "dataTypeName", noticeResult);
+  SET_ER(PG_DIAG_CONSTRAINT_NAME, "constraintName", noticeResult);
+#endif
+  SET_ER(PG_DIAG_SOURCE_FILE, "sourceFile", noticeResult);
+  SET_ER(PG_DIAG_SOURCE_LINE, "sourceLine", noticeResult);
+  SET_ER(PG_DIAG_SOURCE_FUNCTION, "sourceFunction", noticeResult);
+
+  v8::Local<v8::Value> info[2] = {
+    Nan::New<v8::String>("notice").ToLocalChecked(),
+    result,
+  };
+
+  TRACE("CALLING EMIT \"notice\"");
+
+  Nan::TryCatch tc;
+  Nan::AsyncResource *async_emit_f = new Nan::AsyncResource("libpq:connection:emit");
+  async_emit_f->runInAsyncScope(self->handle(), "emit", 2, info);
+  delete async_emit_f;
+  if(tc.HasCaught()) {
+    Nan::FatalException(tc);
+  }
+}
+
 void Connection::on_io_readable(uv_poll_t* handle, int status, int revents) {
   LOG("Connection::on_io_readable");
   TRACEF("Connection::on_io_readable:status %d\n", status);
diff --git a/src/connection.h b/src/connection.h
index ac60d09..6e05c30 100644
--- a/src/connection.h
+++ b/src/connection.h
@@ -66,6 +66,7 @@ class Connection : public Nan::ObjectWrap {
 
     Connection();
 
+    static void notice_receiver(void* connection, const pg_result* result);
     static void on_io_readable(uv_poll_t* handle, int status, int revents);
     static void on_io_writable(uv_poll_t* handle, int status, int revents);
     void ReadStart();
diff --git a/test/notices.js b/test/notices.js
new file mode 100644
index 0000000..7b5602e
--- /dev/null
+++ b/test/notices.js
@@ -0,0 +1,78 @@
+var PQ = require('../');
+var assert = require('assert');
+
+describe('Receive notices', function() {
+  var notice = null;
+
+  before(function() {
+    this.pq = new PQ();
+    this.pq.connectSync();
+    this.pq.exec('SET client_min_messages TO DEBUG');
+
+    this.pq.on('notice', function (arg) {
+      notice = arg;
+    })
+  });
+
+  this.afterEach(function () {
+    notice = null;
+  })
+
+  it('works with "debug" messages', function() {
+    this.pq.exec('DO $$BEGIN RAISE DEBUG \'this is a debug message\'; END$$');
+
+    assert.notEqual(notice, null);
+    assert.equal(notice.severity, 'DEBUG');
+    assert.equal(notice.messagePrimary, 'this is a debug message');
+  });
+
+  it('works with "log" messages', function() {
+    this.pq.exec('DO $$BEGIN RAISE LOG \'this is a log message\'; END$$');
+
+    assert.notEqual(notice, null);
+    assert.equal(notice.severity, 'LOG');
+    assert.equal(notice.messagePrimary, 'this is a log message');
+  });
+
+  it('works with "info" messages', function() {
+    this.pq.exec('DO $$BEGIN RAISE INFO \'this is an info message\'; END$$');
+
+    assert.notEqual(notice, null);
+    assert.equal(notice.severity, 'INFO');
+    assert.equal(notice.messagePrimary, 'this is an info message');
+  });
+
+  it('works with "notice" messages', function() {
+    this.pq.exec('DO $$BEGIN RAISE NOTICE \'this is a notice message\'; END$$');
+
+    assert.notEqual(notice, null);
+    assert.equal(notice.severity, 'NOTICE');
+    assert.equal(notice.messagePrimary, 'this is a notice message');
+  });
+
+  it('works with "warning" messages', function() {
+    this.pq.exec('DO $$BEGIN RAISE WARNING \'this is a warning message\'; END$$');
+
+    assert.notEqual(notice, null);
+    assert.equal(notice.severity, 'WARNING');
+    assert.equal(notice.messagePrimary, 'this is a warning message');
+  });
+
+  it('ignores "exception" messages', function() {
+    this.pq.exec('DO $$BEGIN RAISE EXCEPTION \'this is an exception message\'; END$$');
+
+    assert.equal(notice, null);
+  });
+
+  it('works with internally-generated messages', function() {
+    this.pq.exec('ROLLBACK');
+
+    assert.notEqual(notice, null);
+    assert.equal(notice.severity, 'WARNING');
+    assert.equal(typeof notice.messagePrimary, 'string'); // might be localized
+  });
+
+  after(function() {
+    this.pq.finish();
+  });
+});