Skip to content

Commit 37d1574

Browse files
committed
Add 'packages/pg-cursor/' from commit '492fbdbb65f6f33396d1017fa4cdbbb247dd3895'
git-subtree-dir: packages/pg-cursor git-subtree-mainline: ebb81db git-subtree-split: 492fbdb
2 parents ebb81db + 492fbdb commit 37d1574

16 files changed

+2164
-0
lines changed

packages/pg-cursor/.eslintrc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"extends": ["eslint:recommended"],
3+
"parserOptions": {
4+
"ecmaVersion": 2017
5+
},
6+
"plugins": ["prettier"],
7+
"rules": {
8+
"prettier/prettier": "error",
9+
"prefer-const": "error",
10+
"no-var": "error"
11+
},
12+
"env": {
13+
"es6": true,
14+
"node": true,
15+
"mocha": true
16+
}
17+
}

packages/pg-cursor/.gitignore

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

packages/pg-cursor/.travis.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: node_js
2+
dist: trusty
3+
sudo: false
4+
node_js:
5+
- '8'
6+
- '10'
7+
- '12'
8+
env:
9+
- PGUSER=postgres
10+
services:
11+
- postgresql
12+
addons:
13+
postgresql: '9.6'
14+
before_script:
15+
- psql -c 'create database travis;' -U postgres | true

packages/pg-cursor/Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.PHONY: test
2+
test:
3+
npm test
4+
5+
.PHONY: patch
6+
patch: test
7+
npm version patch -m "Bump version"
8+
git push origin master --tags
9+
npm publish
10+
11+
.PHONY: minor
12+
minor: test
13+
npm version minor -m "Bump version"
14+
git push origin master --tags
15+
npm publish

packages/pg-cursor/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
node-pg-cursor
2+
==============
3+
4+
Use a PostgreSQL result cursor from node with an easy to use API.
5+
6+
### install
7+
8+
```sh
9+
$ npm install pg-cursor
10+
```
11+
___note___: this depends on _either_ `npm install pg` or `npm install pg.js`, but you __must__ be using the pure JavaScript client. This will __not work__ with the native bindings.
12+
13+
### :star: [Documentation](https://node-postgres.com/api/cursor) :star:
14+
15+
### license
16+
17+
The MIT License (MIT)
18+
19+
Copyright (c) 2013 Brian M. Carlson
20+
21+
Permission is hereby granted, free of charge, to any person obtaining a copy
22+
of this software and associated documentation files (the "Software"), to deal
23+
in the Software without restriction, including without limitation the rights
24+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25+
copies of the Software, and to permit persons to whom the Software is
26+
furnished to do so, subject to the following conditions:
27+
28+
The above copyright notice and this permission notice shall be included in
29+
all copies or substantial portions of the Software.
30+
31+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
37+
THE SOFTWARE.

packages/pg-cursor/index.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
'use strict'
2+
const Result = require('pg/lib/result.js')
3+
const prepare = require('pg/lib/utils.js').prepareValue
4+
const EventEmitter = require('events').EventEmitter
5+
const util = require('util')
6+
7+
let nextUniqueID = 1 // concept borrowed from org.postgresql.core.v3.QueryExecutorImpl
8+
9+
function Cursor(text, values, config) {
10+
EventEmitter.call(this)
11+
12+
this._conf = config || {}
13+
this.text = text
14+
this.values = values ? values.map(prepare) : null
15+
this.connection = null
16+
this._queue = []
17+
this.state = 'initialized'
18+
this._result = new Result(this._conf.rowMode, this._conf.types)
19+
this._cb = null
20+
this._rows = null
21+
this._portal = null
22+
this._ifNoData = this._ifNoData.bind(this)
23+
this._rowDescription = this._rowDescription.bind(this)
24+
}
25+
26+
util.inherits(Cursor, EventEmitter)
27+
28+
Cursor.prototype._ifNoData = function() {
29+
this.state = 'idle'
30+
this._shiftQueue()
31+
}
32+
33+
Cursor.prototype._rowDescription = function() {
34+
if (this.connection) {
35+
this.connection.removeListener('noData', this._ifNoData)
36+
}
37+
}
38+
39+
Cursor.prototype.submit = function(connection) {
40+
this.connection = connection
41+
this._portal = 'C_' + nextUniqueID++
42+
43+
const con = connection
44+
45+
con.parse(
46+
{
47+
text: this.text,
48+
},
49+
true
50+
)
51+
52+
con.bind(
53+
{
54+
portal: this._portal,
55+
values: this.values,
56+
},
57+
true
58+
)
59+
60+
con.describe(
61+
{
62+
type: 'P',
63+
name: this._portal, // AWS Redshift requires a portal name
64+
},
65+
true
66+
)
67+
68+
con.flush()
69+
70+
if (this._conf.types) {
71+
this._result._getTypeParser = this._conf.types.getTypeParser
72+
}
73+
74+
con.once('noData', this._ifNoData)
75+
con.once('rowDescription', this._rowDescription)
76+
}
77+
78+
Cursor.prototype._shiftQueue = function() {
79+
if (this._queue.length) {
80+
this._getRows.apply(this, this._queue.shift())
81+
}
82+
}
83+
84+
Cursor.prototype._closePortal = function() {
85+
// because we opened a named portal to stream results
86+
// we need to close the same named portal. Leaving a named portal
87+
// open can lock tables for modification if inside a transaction.
88+
// see https://github.com/brianc/node-pg-cursor/issues/56
89+
this.connection.close({ type: 'P', name: this._portal })
90+
this.connection.sync()
91+
}
92+
93+
Cursor.prototype.handleRowDescription = function(msg) {
94+
this._result.addFields(msg.fields)
95+
this.state = 'idle'
96+
this._shiftQueue()
97+
}
98+
99+
Cursor.prototype.handleDataRow = function(msg) {
100+
const row = this._result.parseRow(msg.fields)
101+
this.emit('row', row, this._result)
102+
this._rows.push(row)
103+
}
104+
105+
Cursor.prototype._sendRows = function() {
106+
this.state = 'idle'
107+
setImmediate(() => {
108+
const cb = this._cb
109+
// remove callback before calling it
110+
// because likely a new one will be added
111+
// within the call to this callback
112+
this._cb = null
113+
if (cb) {
114+
this._result.rows = this._rows
115+
cb(null, this._rows, this._result)
116+
}
117+
this._rows = []
118+
})
119+
}
120+
121+
Cursor.prototype.handleCommandComplete = function(msg) {
122+
this._result.addCommandComplete(msg)
123+
this._closePortal()
124+
}
125+
126+
Cursor.prototype.handlePortalSuspended = function() {
127+
this._sendRows()
128+
}
129+
130+
Cursor.prototype.handleReadyForQuery = function() {
131+
this._sendRows()
132+
this.state = 'done'
133+
this.emit('end', this._result)
134+
}
135+
136+
Cursor.prototype.handleEmptyQuery = function() {
137+
this.connection.sync()
138+
}
139+
140+
Cursor.prototype.handleError = function(msg) {
141+
this.connection.removeListener('noData', this._ifNoData)
142+
this.connection.removeListener('rowDescription', this._rowDescription)
143+
this.state = 'error'
144+
this._error = msg
145+
// satisfy any waiting callback
146+
if (this._cb) {
147+
this._cb(msg)
148+
}
149+
// dispatch error to all waiting callbacks
150+
for (let i = 0; i < this._queue.length; i++) {
151+
this._queue.pop()[1](msg)
152+
}
153+
154+
if (this.listenerCount('error') > 0) {
155+
// only dispatch error events if we have a listener
156+
this.emit('error', msg)
157+
}
158+
// call sync to keep this connection from hanging
159+
this.connection.sync()
160+
}
161+
162+
Cursor.prototype._getRows = function(rows, cb) {
163+
this.state = 'busy'
164+
this._cb = cb
165+
this._rows = []
166+
const msg = {
167+
portal: this._portal,
168+
rows: rows,
169+
}
170+
this.connection.execute(msg, true)
171+
this.connection.flush()
172+
}
173+
174+
// users really shouldn't be calling 'end' here and terminating a connection to postgres
175+
// via the low level connection.end api
176+
Cursor.prototype.end = util.deprecate(function(cb) {
177+
if (this.state !== 'initialized') {
178+
this.connection.sync()
179+
}
180+
this.connection.once('end', cb)
181+
this.connection.end()
182+
}, 'Cursor.end is deprecated. Call end on the client itself to end a connection to the database.')
183+
184+
Cursor.prototype.close = function(cb) {
185+
if (this.state === 'done') {
186+
if (cb) {
187+
return setImmediate(cb)
188+
} else {
189+
return
190+
}
191+
}
192+
this._closePortal()
193+
this.state = 'done'
194+
if (cb) {
195+
this.connection.once('closeComplete', function() {
196+
cb()
197+
})
198+
}
199+
}
200+
201+
Cursor.prototype.read = function(rows, cb) {
202+
if (this.state === 'idle') {
203+
return this._getRows(rows, cb)
204+
}
205+
if (this.state === 'busy' || this.state === 'initialized') {
206+
return this._queue.push([rows, cb])
207+
}
208+
if (this.state === 'error') {
209+
return setImmediate(() => cb(this._error))
210+
}
211+
if (this.state === 'done') {
212+
return setImmediate(() => cb(null, []))
213+
} else {
214+
throw new Error('Unknown state: ' + this.state)
215+
}
216+
}
217+
218+
module.exports = Cursor

packages/pg-cursor/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "pg-cursor",
3+
"version": "2.0.1",
4+
"description": "Query cursor extension for node-postgres",
5+
"main": "index.js",
6+
"directories": {
7+
"test": "test"
8+
},
9+
"scripts": {
10+
"test": " mocha && eslint ."
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "git://github.com/brianc/node-pg-cursor.git"
15+
},
16+
"author": "Brian M. Carlson",
17+
"license": "MIT",
18+
"devDependencies": {
19+
"eslint": "^6.5.1",
20+
"eslint-config-prettier": "^6.4.0",
21+
"eslint-plugin-prettier": "^3.1.1",
22+
"mocha": "^6.2.2",
23+
"pg": "7.x",
24+
"prettier": "^1.18.2"
25+
},
26+
"prettier": {
27+
"semi": false,
28+
"printWidth": 120,
29+
"trailingComma": "es5",
30+
"singleQuote": true
31+
}
32+
}

packages/pg-cursor/test/close.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const assert = require('assert')
2+
const Cursor = require('../')
3+
const pg = require('pg')
4+
5+
const text = 'SELECT generate_series as num FROM generate_series(0, 50)'
6+
describe('close', function() {
7+
beforeEach(function(done) {
8+
const client = (this.client = new pg.Client())
9+
client.connect(done)
10+
client.on('drain', client.end.bind(client))
11+
})
12+
13+
it('can close a finished cursor without a callback', function(done) {
14+
const cursor = new Cursor(text)
15+
this.client.query(cursor)
16+
this.client.query('SELECT NOW()', done)
17+
cursor.read(100, function(err) {
18+
assert.ifError(err)
19+
cursor.close()
20+
})
21+
})
22+
23+
it('closes cursor early', function(done) {
24+
const cursor = new Cursor(text)
25+
this.client.query(cursor)
26+
this.client.query('SELECT NOW()', done)
27+
cursor.read(25, function(err) {
28+
assert.ifError(err)
29+
cursor.close()
30+
})
31+
})
32+
33+
it('works with callback style', function(done) {
34+
const cursor = new Cursor(text)
35+
const client = this.client
36+
client.query(cursor)
37+
cursor.read(25, function(err) {
38+
assert.ifError(err)
39+
cursor.close(function(err) {
40+
assert.ifError(err)
41+
client.query('SELECT NOW()', done)
42+
})
43+
})
44+
})
45+
})

0 commit comments

Comments
 (0)