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(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 result = Nan::New(); + 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 info[2] = { + Nan::New("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(); + }); +});