Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secondary index scan #116

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,15 @@ GameScore
.exec(callback);
```

and scan

```js
GameScore
.scan()
.usingIndex('GameTitleIndex')
.exec(callback);
```

When can also configure the attributes projected into the index.
By default all attributes will be projected when no Projection pramater is
present
Expand Down Expand Up @@ -872,6 +881,15 @@ BlogPost
.exec(callback);
```

and scan

```js
BlogPost
.scan()
.usingIndex('PublishedIndex')
.exec(callback);
```

Could also query for published posts, but this time return oldest first

```js
Expand Down Expand Up @@ -1054,6 +1072,150 @@ User.scan()
.exec();
```

### Indexes

#### Global Indexes

First, define a model with a global secondary index.

```js
var GameScore = dynogels.define('GameScore', {
hashKey : 'userId',
rangeKey : 'gameTitle',
schema : {
userId : Joi.string(),
gameTitle : Joi.string(),
topScore : Joi.number(),
topScoreDateTime : Joi.date(),
wins : Joi.number(),
losses : Joi.number()
},
indexes : [{
hashKey : 'gameTitle', rangeKey : 'topScore', name : 'GameTitleIndex', type : 'global'
}]
});
```

Now we can query against the global index

```js
GameScore
.query('Galaxy Invaders')
.usingIndex('GameTitleIndex')
.descending()
.exec(callback);
```

and scan

```js
GameScore
.scan()
.usingIndex('GameTitleIndex')
.exec(callback);
```

When can also configure the attributes projected into the index.
By default all attributes will be projected when no Projection pramater is
present

```js
var GameScore = dynogels.define('GameScore', {
hashKey : 'userId',
rangeKey : 'gameTitle',
schema : {
userId : Joi.string(),
gameTitle : Joi.string(),
topScore : Joi.number(),
topScoreDateTime : Joi.date(),
wins : Joi.number(),
losses : Joi.number()
},
indexes : [{
hashKey : 'gameTitle',
rangeKey : 'topScore',
name : 'GameTitleIndex',
type : 'global',
projection: { NonKeyAttributes: [ 'wins' ], ProjectionType: 'INCLUDE' } //optional, defaults to ALL

}]
});
```

Filter items against the configured rangekey for the global index.

```js
GameScore
.query('Galaxy Invaders')
.usingIndex('GameTitleIndex')
.where('topScore').gt(1000)
.descending()
.exec(function (err, data) {
console.log(_.map(data.Items, JSON.stringify));
});
```

#### Local Secondary Indexes
First, define a model using a local secondary index

```js
var BlogPost = dynogels.define('Account', {
hashKey : 'email',
rangekey : 'title',
schema : {
email : Joi.string().email(),
title : Joi.string(),
content : Joi.binary(),
PublishedDateTime : Joi.date()
},

indexes : [{
hashkey : 'email', rangekey : 'PublishedDateTime', type : 'local', name : 'PublishedIndex'
}]
});
```

Now we can query for blog posts using the secondary index

```js
BlogPost
.query('werner@example.com')
.usingIndex('PublishedIndex')
.descending()
.exec(callback);
```

and scan

```js
BlogPost
.scan()
.usingIndex('PublishedIndex')
.exec(callback);
```

Could also query for published posts, but this time return oldest first

```js
BlogPost
.query('werner@example.com')
.usingIndex('PublishedIndex')
.ascending()
.exec(callback);
```

Finally lets load all published posts sorted by publish date
```js
BlogPost
.query('werner@example.com')
.usingIndex('PublishedIndex')
.descending()
.loadAll()
.exec(callback);
```

Learn more about [secondary indexes][3]

### Parallel Scan
Parallel scans increase the throughput of your table scans.
The parallel scan operation is identical to the scan api.
Expand Down
6 changes: 6 additions & 0 deletions lib/query-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ module.exports = {
return this;
},

usingIndex(name) {
this.request.IndexName = name;

return this;
},

startKey(hashKey, rangeKey) {
this.request.ExclusiveStartKey = this.serializer.buildKey(hashKey, rangeKey, this.table.schema);

Expand Down
6 changes: 0 additions & 6 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,6 @@ const Query = module.exports = function (hashKey, table, serializer) {
Query.prototype = Object.create(queryBase);
Query.prototype.constructor = Query;

Query.prototype.usingIndex = function (name) {
this.request.IndexName = name;

return this;
};

Query.prototype.consistentRead = function (read) {
if (!_.isBoolean(read)) {
read = true;
Expand Down
61 changes: 60 additions & 1 deletion test/integration/integration-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ describe('Dynogels Integration Tests', function () {
PublishedDateTime: Joi.date().default(Date.now, 'now')
},
indexes: [
{ hashKey: 'UserId', rangeKey: 'PublishedDateTime', type: 'local', name: 'PublishedDateTimeIndex' }
{
hashKey: 'UserId',
rangeKey: 'PublishedDateTime',
type: 'local',
name: 'PublishedDateTimeIndex',
projection: { NonKeyAttributes: ['TweetID', 'content'], ProjectionType: 'INCLUDE' }
}
]
});

Expand Down Expand Up @@ -870,6 +876,59 @@ describe('Dynogels Integration Tests', function () {
});
});

it('should return tweets using secondaryIndex', (done) => {
Tweet.scan()
.usingIndex('PublishedDateTimeIndex')
.exec((err, data) => {
expect(err).to.not.exist;
expect(data.Items).to.have.length.above(0);

let prev;
_.each(data.Items, (t) => {
const published = t.get('PublishedDateTime');

if (prev) {
expect(published < prev).to.be.true;
}

expect(t.toJSON()).to.have.keys(['UserId', 'TweetID', 'content', 'PublishedDateTime']);
expect(t.toJSON()).to.not.have.keys(['num', 'tag']);

prev = published;
});

return done();
});
});

it('should return tweets using secondaryIndex and date object', (done) => {
const oneMinAgo = new Date(new Date().getTime() - (60 * 1000));

Tweet.scan()
.usingIndex('PublishedDateTimeIndex')
.where('PublishedDateTime').gt(oneMinAgo)
.exec((err, data) => {
expect(err).to.not.exist;
expect(data.Items).to.have.length.above(0);

let prev;
_.each(data.Items, (t) => {
const published = t.get('PublishedDateTime');

if (prev) {
expect(published < prev).to.be.true;
}

expect(t.toJSON()).to.have.keys(['UserId', 'TweetID', 'content', 'PublishedDateTime']);
expect(t.toJSON()).to.not.have.keys(['num', 'tag']);

prev = published;
});

return done();
});
});

it('should return users that match expression filters', (done) => {
User.scan()
.filterExpression('#age BETWEEN :low AND :high AND begins_with(#email, :e)')
Expand Down
47 changes: 47 additions & 0 deletions test/scan-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,51 @@ describe('Scan', () => {
scan.request.ProjectionExpression.should.eql('#name, #email');
});
});

describe('#usingIndex', () => {
it('should set the index name to use', () => {
const config = {
hashKey: 'name',
rangeKey: 'email',
schema: {
name: Joi.string(),
email: Joi.string(),
created: Joi.date()
},
indexes: [{ hashKey: 'name', rangeKey: 'created', type: 'local', name: 'CreatedIndex' }]
};

table.schema = new Schema(config);

const scan = new Scan('tim', table, serializer).usingIndex('CreatedIndex');

scan.request.IndexName.should.equal('CreatedIndex');
});

it('should create key condition for global index hash key', () => {
const config = {
hashKey: 'name',
rangeKey: 'email',
schema: {
name: Joi.string(),
email: Joi.string(),
age: Joi.number()
},
indexes: [{ hashKey: 'age', type: 'global', name: 'UserAgeIndex' }]
};

table.schema = new Schema(config);

serializer.serializeItem.returns({ age: { N: '18' } });

const scan = new Scan(table, serializer).usingIndex('UserAgeIndex');
scan.exec();

scan.request.IndexName.should.equal('UserAgeIndex');

scan.request.ExpressionAttributeNames.should.eql({ '#age': 'age' });
scan.request.ExpressionAttributeValues.should.eql({ ':age': 18 });
scan.request.KeyConditionExpression.should.eql('(#age = :age)');
});
});
});