Skip to content

Commit

Permalink
Version 0.2.3 - Changed the behavior of commit() and rollback(). Adde…
Browse files Browse the repository at this point in the history
…d more documenation.
  • Loading branch information
bminer committed Dec 29, 2011
1 parent a49709d commit 99dd2c1
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 22 deletions.
110 changes: 93 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# node-mysql-queues

Add your own node-mysql query queues to support transactions and multiple statements
Add your own node-mysql query queues to support transactions and multiple statements.

For use with Node.JS and node-mysql: https://github.com/felixge/node-mysql

Expand Down Expand Up @@ -29,7 +29,7 @@ q.query(...);
q.query(...);
q.execute();

client.query(...); //Will not execute until all queued queries completed.
client.query(...); //Will not execute until all queued queries (and their callbacks) completed.

//Now you want a transaction?
var trans = client.startTransaction();
Expand All @@ -49,28 +49,42 @@ trans.execute();
client.query("SELECT ...") //This won't execute until the transaction is COMPLETELY done (including callbacks)
```

//Or... as of version 0.2.3, you can do this...
var trans = client.startTransaction();
function error(err) {
if(err) {trans.rollback(); throw err;}
}
trans.query("DELETE...", [x], error);
for(var i = 0; i < n; i++)
trans.query("INSERT...", [ y[i] ], error);
trans.commit();
/* In the case written above, COMMIT is placed at the end of the Queue, yet the
entire transaction can be rolled back if an error occurs. Nesting these queries
was not required. */

//Even multiple Queues work! They get executed in the order that `execute()` is called.
## API

#### `client.query(sql, [params, cb])`
#### client.query(sql, [params, cb])

Use normally. Same as node-mysql, except that if a Queue is still pending
completion, this query may be queued for later execution.

#### `client.createQueue()`
#### client.createQueue()

Creates a new query Queue.

#### `client.startTransaction()`
#### client.startTransaction()

Creates a new query Queue with "START TRANSACTION" as the first queued query.
The Queue object will also have `commit()` and `rollback()` methods.

#### `Queue.query(sql, [params, cb])`
#### Queue.query(sql, [params, cb])

Same as node-mysql. This query will be queued for execution until `execute()`
is called on the `Queue`.

#### `Queue.execute()`
#### Queue.execute()

Executes all queries that were queued using `Queue.query`. Until all query
*callbacks* complete, it is guaranteed that all queries in this Queue
Expand All @@ -83,35 +97,97 @@ call. Thus, nested query queueing is supported in query callbacks, allowing
support for transactions and more.
See the source code for further documentation.

Calling `execute()` on an already executing Queue has no effect.

Note: Once `execute()` is called and all queries have completed, the Queue
will be empty again. You may continue to use `Queue.query` and `Queue.execute`
to queue and execute more queries. However, as noted below, you should
*never* reuse a Queue created by `client.startTransaction`
will be empty again, returning control to either: (a) another Queue that has been
queued for execution; or (b) the main MySQL queue (a.k.a. queries executed
with `client.query`). Once a Queue is empty and has finished executing, you may
continue to use `Queue.query` and `Queue.execute` to queue and execute more
queries; however, as noted below, you should *never* reuse a Queue created by
`client.startTransaction`

#### `Queue.commit()`
#### Queue.commit()

Available only if this Queue was created with `client.startTransaction`.
This queues 'COMMIT' and calls `execute()`
This queues 'COMMIT' and calls `execute()`.
You should call either `commit()` or `rollback()` exactly once. Once you call
`commit()` on this Queue, you should discard it.

As of version 0.2.3, it is now possible to call `rollback()` even after
`commit()` has been called. In a typical scenario, you want your query
callbacks to call `rollback()` if an error occurred (i.e. a foreign key
constraint was violated). If no error occurs, you want to call `commit()`.
There are some situations when you execute multiple queries "at the same time"
and you want to commit all of them if and only if they succeed. This allows
you to avoid nesting these queries, queue them up, queue up COMMIT, and
execute `rollback()` only if an error occurs.

If you do not call `commit()` or `rollback()` and the Queue has completed
execution, `commit()` will be called automatically; however, one should
**NOT** rely on this behavior. In fact, mysql-queues will print nasty
warning messages if you do not explicitly `commit()` or `rollback()` a
transaction.

#### `Queue.rollback()`
#### Queue.rollback()

Available only if this Queue was created with `client.startTransaction`.
This queues 'ROLLBACK' and calls `execute()`
You should call either `commit()` or `rollback()` exactly once. Once you call
`rollback()` on this Queue, you should discard it.
This executes 'ROLLBACK' immediately and purges the remaining queries in the
queue. You should call either `commit()` or `rollback()` exactly once. Once
you call `rollback()` on this Queue, you should discard it.

Note: Before 0.2.3, `rollback()` would add the 'ROLLBACK' query to the Queue
and the Queue would continue executing. This was changed in 0.2.3 because it
is more natural for a ROLLBACK operation to abort the remaining Queue, since
it will be rolled back anyway. As mentioned above, this also allows you to
queue the COMMIT query at the bottom of the queue, and if an error occurs
before the COMMIT, you can safely `rollback()` the entire transaction.

### `require('mysql-queues')(client, debug)`
### require('mysql-queues')(client, debug)

Attaches mysql-queues to the mysql client. When `debug` mode is enabled,
debugging messages are printed to standard error when certain exceptions occur.
When you queue a query, the call stack becomes somewhat useless, and it can
become difficult to determine which query is causing a problem. The debug
feature allows you to more easily determine which query that caused a problem.

## Don't do this...

```javascript
//You may be tempted to do this...
var fs = require('fs');
var trans = db.startTransaction();
trans.query("INSERT ...", [...], function(err, info) {
fs.readFile("foobar.txt", function(err, data) {
//By now, it's too late to use `trans`
if(data == "something")
trans.commit();
else
trans.rollback();
});
//The query callback is now done!! This is your last chance
//to call `commit` or `rollback`
}).execute();
```

In the case above, an asynchronous call was placed in the query callback.
This won't work as expected. The query callback completes and automatically
executes `commit()` before the asychronous filesystem call completes. In this
example, you will get a warning message, but that's it! The problem will run
normally otherwise, but the expected behavior will not produced.

To be clear, this problem is limited by aynchronous file I/O operations; even
a query to another database will cause this problem (i.e. if you execute a
series of MySQL queries and then update Redis, for example)

Possible workarounds include:
- Using synchronous I/O operations (i.e. readFileSync in this case)
- Performing your asynchronous operation BEFORE you execute any queued
queries.

Limitations:
- As far as I know, you can't use mysql-queues to run a MySQL query, run a
Redis command, and then `commit()` if and only if neither return an error.
I may add a `pause()` method, which will pause a Queue and all queries until
`resume()` is called. This would allow you to `pause()` right before the
Redis command and `resume()` when the Redis command is completed.
9 changes: 5 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = function(db, debug) {
}, debug);
}
db.startTransaction = function() {
return Queue.isNowTransaction(this.createQueue() );
return Queue.isNowTransaction(this.createQueue(), function() {return dbQuery.apply(db, arguments);});
}
}
function Queue(dbQuery, resumeMainQueue, debug) {
Expand Down Expand Up @@ -107,6 +107,7 @@ function Queue(dbQuery, resumeMainQueue, debug) {
throw e;
}
})(that.queue[i]);
if(that.queue.length == 0) return; //A rollback must have occurred!
}
that.queue = [];
//All queued queries are running, but we don't resume the main queue just yet
Expand All @@ -115,16 +116,16 @@ function Queue(dbQuery, resumeMainQueue, debug) {
return this; //Chaining :)
};
}
Queue.isNowTransaction = function(q) {
Queue.isNowTransaction = function(q, dbQuery) {
q.query("START TRANSACTION");
q.commit = function(cb) {
this.query("COMMIT", cb);
delete this.commit;
delete this.rollback;
this.execute();
}
q.rollback = function(cb) {
this.query("ROLLBACK", cb);
this.queue = [];
dbQuery("ROLLBACK", cb);
delete this.commit;
delete this.rollback;
this.execute();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"author": "Blake Miner <miner.blake@gmail.com> (http://www.blakeminer.com)",
"name": "mysql-queues",
"description": "Wraps 'mysql' to provide mulitple query queues, allowing support for multiple statements and transactions.",
"version": "0.2.2",
"version": "0.2.3",
"homepage": "https://github.com/bminer/node-mysql-queues",
"repository": {
"type": "git",
Expand Down

0 comments on commit 99dd2c1

Please sign in to comment.