diff --git a/lib/redis.js b/lib/redis.js index 56224555..c475b54b 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -58,7 +58,9 @@ BridgeToRedis.prototype.updateIndexes = function (model, data, callback) { }.bind(this)); if (schedule.length) { - this.client.multi(schedule).exec(callback); + this.client.multi(schedule).exec(function (err) { + callback(err); + }); } else { callback(null); } @@ -100,13 +102,13 @@ BridgeToRedis.prototype.destroy = function destroy(model, id, callback) { }); }; -BridgeToRedis.prototype.possibleIndex = function possibleIndex(model, filter) { +BridgeToRedis.prototype.possibleIndexes = function (model, filter) { if (!filter || Object.keys(filter).length === 0) return false; - var foundIndex = false; + var foundIndex = []; Object.keys(filter).forEach(function (key) { - if (!foundIndex && this.indexes[model][key]) { - foundIndex = key; + if (this.indexes[model][key]) { + foundIndex.push('i:' + model + ':' + key + ':' + filter[key]); } }.bind(this)); @@ -115,26 +117,24 @@ BridgeToRedis.prototype.possibleIndex = function possibleIndex(model, filter) { BridgeToRedis.prototype.all = function all(model, filter, callback) { var ts = Date.now(); + var client = this.client; - var index = this.possibleIndex(model, filter); - if (false) { - // console.log('using index!', filter); - this.client.smembers('i:' + model + ':' + index + ':' + filter[index], - handleKeys.bind(this)); + var indexes = this.possibleIndexes(model, filter); + if (indexes.length) { + indexes.push(handleKeys); + client.sinter.apply(client, indexes); } else { - // console.log('without index', filter); - this.client.keys(model + ':*', handleKeys.bind(this)); + client.keys(model + ':*', handleKeys); } function handleKeys(err, keys) { - // console.log(arguments); if (err) { return callback(err, []); } var query = keys.map(function (key) { return ['hgetall', key]; }); - this.client.multi(query).exec(function (err, replies) { + client.multi(query).exec(function (err, replies) { // console.log('Redis time: %dms', Date.now() - ts); callback(err, filter ? replies.filter(applyFilter(filter)) : replies); }); diff --git a/test/common_test.js b/test/common_test.js index 2a6f22d2..49be4827 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -27,12 +27,12 @@ Object.keys(schemas).forEach(function (schemaName) { function testOrm(schema) { - var Post; + var Post, User; var start = Date.now(); it('should define class', function (test) { - var User = schema.define('User', { + User = schema.define('User', { name: String, bio: Text, approved: Boolean, @@ -47,6 +47,18 @@ function testOrm(schema) { published: { type: Boolean, default: false } }); + User.hasMany(Post, {as: 'posts', foreignKey: 'userId'}); + // creates instance methods: + // user.posts(conds) + // user.buildPost(data) // like new Post({userId: user.id}); + // user.createPost(data) // build and save + + Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); + // creates instance methods: + // post.author(callback) -- getter when called with function + // post.author() -- sync getter when called without params + // post.author(user) -- setter when called with object + var user = new User; test.ok(User instanceof Function); @@ -82,7 +94,7 @@ function testOrm(schema) { it('should be expoted to JSON', function (test) { test.equal(JSON.stringify(new Post({id: 1, title: 'hello, json'})), - '{"id":1,"title":"hello, json","content":null,"date":null,"published":null}'); + '{"id":1,"title":"hello, json","content":null,"date":null,"published":null,"userId":null}'); test.done(); }); @@ -260,6 +272,23 @@ function testOrm(schema) { }); + it('should handle hasMany relationship', function (test) { + User.create(function (err, u) { + if (err) return console.log(err); + test.ok(u.posts, 'Method defined: posts'); + test.ok(u.buildPost, 'Method defined: buildPost'); + test.ok(u.createPost, 'Method defined: createPost'); + u.createPost(function (err, post) { + if (err) return console.log(err); + test.ok(post.author(), u.id); + u.posts(function (err, posts) { + test.strictEqual(posts.pop(), post); + test.done(); + }); + }); + }); + }); + it('should destroy all records', function (test) { Post.destroyAll(function (err) { Post.all(function (err, posts) { diff --git a/test/datamapper_test.js b/test/datamapper_test.js deleted file mode 100644 index 11047d34..00000000 --- a/test/datamapper_test.js +++ /dev/null @@ -1,317 +0,0 @@ -require('./spec_helper').init(exports); - -[ 'redis~' -, 'mysql' -, 'mongodb~' -, 'postgres~' -].forEach(function (driver) { - // context(driver, testCasesFor(driver)); -}); - -function testCasesFor (driver) { - return function () { - - function Post () { this.initialize.apply(this, Array.prototype.slice.call(arguments)); } - function Comment () { this.initialize.apply(this, Array.prototype.slice.call(arguments)); } - - var properties = {}; - - properties['post'] = { - title: { type: String, validate: /.{10,255}/ }, - content: { type: String }, - published: { type: Boolean, default: false }, - date: { type: Date, default: function () {return new Date} } - }; - - properties['comment'] = { - content: { type: String, validate: /./ }, - date: { type: Date }, - author: { type: String }, - approved: { type: Boolean } - }; - - var associations = {}; - associations['post'] = { - comments: {className: 'Comment', relationType: 'n', tableName: 'comment'} - }; - - associations['comment'] = { - post: {className: 'Post', relationType: '<', tableName: 'post'} - }; - - try { - var orm = require('../lib/datamapper/' + driver); - if (driver == 'mysql') { - orm.configure({ - host: 'webdesk.homelinux.org', - port: 3306, - database: 'test', - user: 'guest', - password: '' - }); - } - } catch (e) { - console.log(e.message); - return; - } - orm.debugMode = true; - orm.mixPersistMethods(Post, { - className: 'Post', - tableName: 'post', - properties: properties['post'], - associations: associations['post'] - }); - orm.mixPersistMethods(Comment, { - className: 'Comment', - tableName: 'comment', - properties: properties['comment'], - associations: associations['comment'], - scopes: { - approved: { conditions: { approved: true } }, - author: { block: function (author) { return {conditions: {author: author}}; } } - } - }); - - var HOW_MANY_RECORDS = 1; - - it('cleanup database', function (test) { - var wait = 0; - var time = new Date; - var len; - Post.allInstances(function (posts) { - if (posts.length === 0) test.done(); - len = posts.length; - posts.forEach(function (post) { - wait += 1; - post.destroy(done); - }); - }); - - function done () { - if (--wait === 0) { - test.done(); - console.log('Cleanup %d records completed in %d ms', len, new Date - time); - } - } - }); - - it('create a lot of data', function (test) { - var wait = HOW_MANY_RECORDS; - var time = new Date; - for (var i = wait; i > 0; i -= 1) { - Post.create({title: Math.random().toString(), content: arguments.callee.caller.toString(), date: new Date, published: false}, done); - } - - function done () { - if (--wait === 0) { - test.done(); - console.log('Creating %d records completed in %d ms', HOW_MANY_RECORDS, new Date - time); - } - } - }); - - it('should retrieve all data fast', function (test) { - var time = new Date; - Post.allInstances(function (posts) { - test.equal(posts.length, HOW_MANY_RECORDS); - console.log('Retrieving %d records completed in %d ms', HOW_MANY_RECORDS, new Date - time); - test.done(); - }); - }); - - it('should initialize object properly', function (test) { - var hw = 'Hello world', post = new Post({title: hw}); - test.equal(post.title, hw); - test.ok(!post.propertyChanged('title')); - post.title = 'Goodbye, Lenin'; - test.equal(post.title_was, hw); - test.ok(post.propertyChanged('title')); - test.ok(post.isNewRecord()); - test.done(); - }); - - it('should create object', function (test) { - Post.create(function () { - test.ok(this.id); - Post.exists(this.id, function (exists) { - test.ok(exists); - test.done(); - }); - }); - }); - - it('should save object', function (test) { - var title = 'Initial title', title2 = 'Hello world', - date = new Date; - - Post.create({ - title: title, - date: date - }, function () { - test.ok(this.id); - test.equals(this.title, title); - test.equals(this.date, date); - this.title = title2; - this.save(function () { - test.equal(this.title, title2); - test.ok(!this.propertyChanged('title')); - test.done(); - }); - }); - }); - - it('should create object with initial data', function (test) { - var title = 'Initial title', - date = new Date; - - Post.create({ - title: title, - date: date - }, function () { - test.ok(this.id); - test.equals(this.title, title); - test.equals(this.date, date); - Post.find(this.id, function () { - test.equal(this.title, title); - test.equal(this.date, date.toString()); - test.done(); - }); - }); - }); - - it('should not create new instances for the same object', function (test) { - var title = 'Initial title'; - Post.create({ title: title }, function () { - var post = this; - test.ok(this.id, 'Object should have id'); - test.equals(this.title, title); - Post.find(this.id, function () { - test.equal(this.title, title); - test.strictEqual(this, post); - test.done(); - }); - }); - }); - - it('should destroy object', function (test) { - Post.create(function () { - var post = this; - Post.exists(post.id, function (exists) { - test.ok(exists, 'Object exists'); - post.destroy(function () { - Post.exists(post.id, function (exists) { - test.ok(!exists, 'Object not exists'); - Post.find(post.id, function (err, obj) { - test.ok(err, 'Object not found'); - test.equal(obj, null, 'Param obj should be null'); - test.done(); - }); - }); - }); - }); - }); - }); - - it('should update single attribute', function (test) { - Post.create({title: 'title', content: 'content'}, function () { - this.content = 'New content'; - this.updateAttribute('title', 'New title', function () { - test.equal(this.title, 'New title'); - test.ok(!this.propertyChanged('title')); - test.equal(this.content, 'New content'); - test.ok(this.propertyChanged('content')); - this.reload(function () { - test.equal(this.title, 'New title'); - test.ok(!this.propertyChanged('title')); - test.equal(this.content, 'content'); - test.ok(!this.propertyChanged('content')); - test.done(); - }); - }); - }); - }); - - // NOTE: this test rely on previous - it('should fetch collection', function (test) { - Post.allInstances(function (posts) { - test.ok(posts.length > 0); - test.strictEqual(posts[0].constructor, Post); - test.done(); - }); - }); - - // NOTE: this test rely on previous - it('should fetch first, second, third and last elements of class', function (test) { - test.done(); return; - var queries = 4; - test.expect(queries); - function done () { if (--queries == 0) test.done(); } - - Post.first(function (post) { - test.strictEqual(post.constructor, Post); - done(); - }); - Post.second(function (post) { - test.strictEqual(post.constructor, Post); - done(); - }); - Post.third(function (post) { - test.strictEqual(post.constructor, Post); - done(); - }); - Post.last(function (post) { - test.strictEqual(post.constructor, Post); - done(); - }); - }); - - it('should load associated collection', function (test) { - test.done(); return; - Post.last(function (post) { - post.comments.approved.where('author = ?', 'me').load(); - }); - }); - - it('should find record and associated association', function (test) { - test.done(); return; - Post.last(function (post) { - Post.find(post.id, {include: 'comments'}, function () { - }); - }); - }); - - it('should fetch associated collection', function (test) { - test.done(); return; - Post.create(function () { - // load collection - this.comments(function () { - }); - // creating associated object - this.comments.create(function () { - }); - this.comments.build().save(); - // named scopes - this.comments.pending(function () { - }); - this.comments.approved(function () { - }); - }); - }); - - it('should validate object', function (test) { - test.done(); return; - var post = new Post; - test.ok(!post.isValid()); - post.save(function (id) { - test.ok(!id, 'Post should not be saved'); - }); - post.title = 'Title'; - test.ok(post.isValid()); - post.save(function (id) { - test.ok(id); - test.done(); - }); - }); - - } -}; diff --git a/test/perf_test.coffee b/test/perf_test.coffee index 4127a1dc..1770edd6 100644 --- a/test/perf_test.coffee +++ b/test/perf_test.coffee @@ -14,8 +14,8 @@ schemas = testOrm = (schema) -> User = Post = 'unknown' - maxUsers = 50 - maxPosts = 10000 + maxUsers = 100 + maxPosts = 50000 users = [] it 'should define simple', (test) -> @@ -28,12 +28,11 @@ testOrm = (schema) -> age: Number } - Post = schema.define('Post', { - title: { type: String, length: 255 } + Post = schema.define 'Post', + title: { type: String, length: 255, index: true } content: { type: Text } date: { type: Date, detault: Date.now } published: { type: Boolean, default: false } - }) User.hasMany(Post, {as: 'posts', foreignKey: 'userId'}) Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}) @@ -52,8 +51,8 @@ testOrm = (schema) -> done = -> test.done() if --wait == 0 rnd = (title) -> { - userId: users[Math.floor(Math.random() * maxUsers)].id, - title: title + userId: users[Math.floor(Math.random() * maxUsers)].id + title: 'Post number ' + (title % 5) } Post.create(rnd(num), done) for num in [1..maxPosts] @@ -62,10 +61,12 @@ testOrm = (schema) -> done = -> test.done() if --wait == 0 ts = Date.now() query = (num) -> - users[num].posts (err, collection) -> + users[num].posts { title: 'Post number 3' }, (err, collection) -> console.log('User ' + num + ':', collection.length, 'posts in', Date.now() - ts,'ms') done() - query num for num in [1..4] + query num for num in [0..4] + + return it 'should destroy all data', (test) -> Post.destroyAll ->