Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Commit

Permalink
release: v0.1.0 with range coercion support
Browse files Browse the repository at this point in the history
  • Loading branch information
benesch committed Apr 7, 2014
1 parent 5472000 commit 65169aa
Show file tree
Hide file tree
Showing 14 changed files with 720 additions and 165 deletions.
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"unused": "vars",

// Relaxing options
"expr": true,
"laxcomma": true,

// Environments
Expand Down
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FILES = $(shell find . -name '*.js' -not -path '*node_modules*')

check:
jshint $(FILES)

test:
mocha

test-watch:
mocha --reporter min --watch --growl

.PHONY: jshint test test-watch
122 changes: 99 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# pg-parse-timeranges
# node-pg-range

Parser for PostgreSQL time ranges (`daterange`, `tsrange`, `tstzrange`).
Range type support for [node-postgres][node-postgres].

## Usage

Load pg-parse-timeranges
Install pg-range into your existing [pg][node-postgres] adapter:

```javascript
var pg = require("pg");
require("pg-parse-timeranges")(pg);
require("pg-range").install(pg);
```

then make a query that returns range objects!
Then make a query that returns range objects!

```javascript
client.query("SELECT range FROM table", function (result) {
Expand All @@ -23,22 +23,89 @@ client.query("SELECT range FROM table", function (result) {
});
```

See the [Postgres "Range Types" documentation][postgres-docs] for details.
Or go the other way:

```javascript
var Range = require("pg-range").Range;

client.query("INSERT INTO table VALUES ($1)", [Range(1, 3)]);
```

See the Postgres ["Range Types" documentation][postgres-docs] for details.

### Range objects

The types the range is made of (either `date`, `timestamp`, or `timestamptz`)
are parsed using whatever parser you've registered for that type. That is, the
types of `lower` and `upper` will vary based on the parser you've registered.
Values of (PostgreSQL) type

* `int4range`
* `int8range`
* `numrange`
* `tsrange`
* `tstzrange`
* `daterange`

will be automatically parsed into instances of `Range`.

#### Construction

Finite integer range with default bounds:

```javascript
Range(1, 3)
```

Explicit bounds:

By default, node-postgres parses all date/time types to instances of the built-
in `Date` object.
```javascript
Range(1, 3, "(]")
Range(1, 7, "()")
```

##### `lower` *Date* | *custom*
Date ranges:

##### `upper` *Date* | *custom*
```javascript
Range(new Date(2014, 0, 1), new Date(2014, 1, 1))
```

##### `bound` *string*
Infinite ranges:

```javascript
Range(1, null)
Range(null, 7)
Range(null, null)
```

Empty range:

```javascript
Range()
```

Range is a naive class; instances need not be valid PostgreSQL ranges or even
sensible. This is perfectly valid

```javascript
Range(-7, new Date(2014, 1, 1))
```

and won't fail until you try to use it in a query.

#### Properties

##### `lower` *varies*

The lower bound of the range.

The type depends on which parser you've registered for the component type. For
example, an `int4range` is made up of the `int4` type. By default, node-postgres
parses `int4`s to a JS numbers. But if you've overriden this parsing with
[pg-types][pg-types], pg-range respects this.

##### `upper` *varies*

The upper bound of the range. See `lower`.

##### `bounds` *string*

A string representing the exclusivity of the lower and upper bounds. A
parenthesis indicates an exclusive bound, while a square bracket indicates an
Expand All @@ -47,17 +114,26 @@ inclusive bound.
Valid values: `()`, `(]`, `[)`, `[]`


## Caveats
#### Methods

Doesn't support coercing range objects on insertion. But the range functions
are pretty convenient:
##### `toJSON`

```javascript
client.query("INSERT INTO table VALUES (tsrange($1, $2, $3))", [
new Date("..."),
new Date("..."),
"[)"
])
Yields an object with the following properties:

```json
{
"lower": "?",
"upper": "?",
"bounds": "?"
}
```


## Caveats

Range objects don't support any useful operators, like `contains`, `leftOf`,
`each`, etc. Maybe for 1.0?

[node-postgres]: https://github.com/brianc/node-postgres
[pg-types]: https://github.com/brianc/node-pg-types
[postgres-docs]: http://www.postgresql.org/docs/9.3/static/rangetypes.html
53 changes: 5 additions & 48 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,7 @@
module.exports = function (pg) {
var RANGE_MATCHER = /(\[|\()("((?:\\"|[^"])*)"|[^"]*),("((?:\\"|[^"])*)"|[^"]*)(\]|\))/;
var Range = require("./lib/range");
var parser = require("./lib/parser");

var types = {
DATE: 1082,
TIMESTAMP: 1114,
TIMESTAMPTZ: 1184,
DATERANGE: 3912,
TSRANGE: 3908,
TSTZRANGE: 3910
};

function parseRangeSegment(whole, quoted) {
if (quoted) {
return quoted.replace(/\\(.)/g, '$1');
}
return whole;
}

function makeRangeParser(dateType) {
var parseDate = pg.types.getTypeParser(dateType, "text");

return function parseRange(val) {
var matches = val.match(RANGE_MATCHER);

if (!matches) {
// empty
return {
"lower": null,
"upper": null,
"bounds": null
};
}

var bounds = matches[1] + matches[6];
var lower = parseRangeSegment(matches[2], matches[3]);
var upper = parseRangeSegment(matches[4], matches[5]);

return {
"lower": parseDate(lower),
"upper": parseDate(upper),
"bounds": bounds
};
};
}

pg.types.setTypeParser(types.DATERANGE, makeRangeParser(types.DATE));
pg.types.setTypeParser(types.TSRANGE, makeRangeParser(types.TIMESTAMP));
pg.types.setTypeParser(types.TSTZRANGE, makeRangeParser(types.TIMESTAMPTZ));
module.exports = {
Range: Range,
install: parser.install
};
69 changes: 69 additions & 0 deletions lib/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
var _ = require("lodash")
, Range = require("./range");

var RANGE_MATCHER = /(\[|\()("((?:\\"|[^"])*)"|[^"]*),("((?:\\"|[^"])*)"|[^"]*)(\]|\))/;

var oids = {
INTEGER: 23,
BIGINT: 20,
NUMERIC: 1700,
TIMESTAMP: 1114,
TIMESTAMPTZ: 1184,
DATE: 1082,

INT4RANGE: 3904,
INT8RANGE: 3926,
NUMRANGE: 3906,
TSRANGE: 3908,
TSTZRANGE: 3910,
DATERANGE: 3912,
};

function parseRangeSegment(whole, quoted) {
if (quoted) {
return quoted.replace(/\\(.)/g, "$1");
}
if (whole === "") {
return null;
}
return whole;
}

function parseRange(parseBound, rangeLiteral) {
var matches = rangeLiteral.match(RANGE_MATCHER);

if (!matches) {
// empty
return Range();
}

var bounds = matches[1] + matches[6];
var lower = parseRangeSegment(matches[2], matches[3]);
var upper = parseRangeSegment(matches[4], matches[5]);

return Range(
lower ? parseBound(lower) : null,
upper ? parseBound(upper) : null,
bounds);
}

function install(pg, rangeOid, subtypeOid) {
var subtypeParser;

if (!rangeOid && !subtypeOid) {
install(pg, oids.INT4RANGE, oids.INTEGER);
install(pg, oids.INT8RANGE, oids.BIGINT);
install(pg, oids.NUMRANGE, oids.NUMERIC);
install(pg, oids.TSRANGE, oids.TIMESTAMP);
install(pg, oids.TSTZRANGE, oids.TIMESTAMPTZ);
install(pg, oids.DATERANGE, oids.DATE);
}

subtypeParser = pg.types.getTypeParser(subtypeOid, "text");
pg.types.setTypeParser(rangeOid, _.partial(parseRange, subtypeParser));
}

module.exports = {
install: install,
parseRange: parseRange
};
65 changes: 65 additions & 0 deletions lib/range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
var util = require("util");

var VALID_BOUNDS = ["[]", "[)", "(]", "()"];

function formatBound(value, prepare) {
if (value === null) {
return "";
}

value = prepare(value);
if (/[()[\],"\\]/.test(value)) {
// quote bound only if necessary
value = "\"" + value.replace(/(\\|")/, "\\$1") + "\"";
}
return value;
}

function Range(lower, upper, bounds) {
if (!(this instanceof Range)) {
return new Range(lower, upper, bounds);
}

if (!lower && !upper && !bounds) {
this.empty = true;
return;
}

if (lower && upper && lower > upper) {
throw new Error("invalid range: lower bound greater than upper bound");
}

bounds = bounds || "[)";
if (VALID_BOUNDS.indexOf(bounds) === -1) {
throw new Error(util.format("invalid bounds: %s", bounds));
}

this.lower = lower;
this.upper = upper;
this.bounds = bounds;
}

Range.prototype.toPostgres = function (prepare) {
if (this.empty) {
return "empty";
}

return util.format("%s%s,%s%s",
this.bounds[0],
formatBound(this.lower, prepare),
formatBound(this.upper, prepare),
this.bounds[1]);
};

Range.prototype.toJSON = function () {
if (this.empty) {
return { lower: null, upper: null, bounds: null };
}
return {
lower: this.lower,
upper: this.upper,
bounds: this.bounds
};
};

module.exports = Range;
Loading

0 comments on commit 65169aa

Please sign in to comment.