├── .gitignore ├── README.md ├── binding.gyp ├── doc ├── examples.html ├── examples.js ├── index.php └── publish.sh ├── index.html ├── speedtest.js ├── sqlite.js ├── sqlite3_bindings.cc ├── sqlite3_bindings.node ├── test.js └── testloop.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | README.html 3 | test.db 4 | .lock-wscript 5 | speedtest.db 6 | node_modules/ 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SQLite3 bindings for Node.js 2 | ============================= 3 | 4 | Documentation lives at http://grumdrig.com/node-sqlite/ 5 | The code lives at http://github.com/grumdrig/node-sqlite/ 6 | 7 | The two files required to use these bindings are sqlite.js and 8 | build/default/sqlite3_bindings.node. Put this directory in your 9 | NODE_PATH or copy those two files where you need them. 10 | 11 | Tested with Node version v8.4.0 (may not work with older versions) 12 | 13 | ### Synchronous Only 14 | 15 | Only synchronous access is supported! For an asynchronous and 16 | much more full-featured library, see 17 | https://github.com/mapbox/node-sqlite3 18 | 19 | ### TODO 20 | 21 | The C++ code which creates the `Statement` object uses `NewInstance`, 22 | which is marked deprecated. I don't know what it should be replaced 23 | with but that whole bit code is pretty ugly and could probably be 24 | improved a lot. 25 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "sqlite3_bindings", 5 | "include_dirs" : [" 3 | pre, code { color: #060; font-size: 11pt; } 4 | pre { margin-left: 2ex; padding: 1ex; background-color: #eee; } 5 | p { font-size: 12pt; } 6 | body { 7 | margin-left: 10%; 8 | margin-right: 10%; 9 | background-color: #fff; 10 | color: black; 11 | max-width: 800px; 12 | } 13 | h1,h2,h3,h4 { font-family: helvetica } 14 | h1 { font-size: 36pt; } 15 | h1 { 16 | background-color: #28b; 17 | color: white; 18 | padding-left:6px; 19 | } 20 | h2 { 21 | color: #28b; 22 | } 23 | 24 | 25 | 26 |
27 |
29 | Import the library and open a database. (Only syncronous database 30 | access is implemented at this time.) 31 |
32 |
33 | var sqlite = require("../sqlite"); 34 | var db = sqlite.openDatabaseSync("example.db"); 35 | var assert = require("assert").ok; 36 |37 |
38 | Perform an SQL query on the database: 39 |
40 |
41 | db.query("CREATE TABLE foo (a,b,c)"); 42 |43 |
44 | This is a more convenient form than the HTML5 syntax for the same 45 | thing, but which is also supported: 46 |
47 |
48 | db.transaction(function(tx) { 49 | tx.executeSql("CREATE TABLE bar (x,y,z)"); 50 | }); 51 |52 |
53 | This allows the same or similar code to work on the client and 54 | server end (modulo browser support of HTML5 Web SQL). 55 |
56 | Transactions generate either a "commit" or "rollback" event. 57 |
58 |
59 | var rollbacks = 0; 60 | db.addListener("rollback", function () { 61 | ++rollbacks; 62 | }); 63 |64 |
65 | Both forms take an optional second parameter which is values to 66 | bind to fields in the query, as an array: 67 |
68 |
69 | db.query("INSERT INTO foo (a,b,c) VALUES (?,?,?)", ['apple','banana',22]); 70 |71 |
72 | or as a map: 73 |
74 |
75 | db.query("INSERT INTO bar (x,y,z) VALUES ($x,$y,$zebra)", 76 | {$x: 10, $y:20, $zebra:"stripes"}); 77 |78 |
79 | Also optional is a callback function which is called with an object 80 | representing the results of the query: 81 |
82 |
83 | db.query("SELECT x FROM bar", function (records) { 84 | assert(records.length == 1); 85 | assert(records[0].x == 10); 86 |87 |
88 | The HTML5 semantics for the record set also work: 89 |
90 |
91 | assert(records.rows.length == 1); 92 | assert(records.rows.item(0).x == 10); 93 | }); 94 |95 |
96 | INSERT, UPDATE & DELETE queries set rowsAffected
on their result
97 | set object:
98 |
99 |
100 | db.query("UPDATE foo SET a = ? WHERE a = ?", ['orange', 'apple'], function(r) { 101 | assert(r.rowsAffected == 1); 102 | }); 103 |104 |
105 | They also emit an "update"
event.
106 |
107 | INSERT queries set insertId
:
108 |
109 |
110 | var insert = db.query("INSERT INTO foo VALUES (1,2,3)"); 111 | assert(insert.insertId == 2); 112 |113 |
114 | Note here that the result set passed to the callback is also
115 | returned by query
.
116 |
117 | Multiple-statement queries are supported; each statement's result set is retuned to the callback as a separate parameter: 118 |
119 |
120 | var q = db.query("UPDATE bar SET z=20; SELECT SUM(z) FROM bar;", 121 | function (update, select) { 122 | assert(update.rowsAffected == 1); 123 | assert(select[0]['SUM(z)'] == 20); 124 | }); 125 |126 |
127 | An array of all result sets is available as the .all
property on
128 | each result set:
129 |
130 |
131 | assert(q.all[1].length == 1); 132 |133 |
134 | HTML5 semantics are supported. 135 |
136 |
137 | db.transaction(function(tx) { 138 | tx.executeSql("SELECT * FROM foo WHERE c = ?", [3], function(tx,res) { 139 | assert(res.rows.item(0).c == 3); 140 | }); 141 | }); 142 |143 |
144 | The query
and transaction
APIs wrap lower level APIs that more
145 | thinly wrap the underlying C api:
146 |
147 |
148 | var stmt = db.prepare("INSERT INTO foo VALUES (?,?,?)"); 149 | stmt.bind(1, "curly"); 150 | stmt.bind(2, "moe"); 151 | stmt.bind(3, "larry"); 152 | stmt.step(); // Insert Curly, Moe & Larry 153 | stmt.reset(); 154 | stmt.step(); // Insert another row with same stooges 155 | stmt.reset(); 156 | stmt.clearBindings(); 157 | stmt.bind(2, "lonely"); 158 | stmt.step(); // Insert (null, "lonely", null) 159 | stmt.finalize(); 160 |161 |
162 | Close it: 163 |
164 |
165 | db.close(); 166 |167 |
168 |
169 | 170 |
171 | 172 | -------------------------------------------------------------------------------- /doc/examples.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | /* 3 | //!! To be processed by docbyex.py, or run by Node 4 | 5 | Documentation by Example :: 6 | */ 7 | 8 | // Import the library and open a database. (Only syncronous database 9 | // access is implemented at this time.) 10 | 11 | var sqlite = require("../sqlite"); 12 | var db = sqlite.openDatabaseSync("example.db"); 13 | var assert = require("assert").ok; 14 | 15 | // Perform an SQL query on the database: 16 | 17 | db.query("CREATE TABLE foo (a,b,c)"); 18 | 19 | // This is a more convenient form than the HTML5 syntax for the same 20 | // thing, but which is also supported: 21 | 22 | db.transaction(function(tx) { 23 | tx.executeSql("CREATE TABLE bar (x,y,z)"); 24 | }); 25 | 26 | // This allows the same or similar code to work on the client and 27 | // server end (modulo browser support of HTML5 Web SQL). 28 | 29 | // Transactions generate either a "commit" or "rollback" event. 30 | 31 | var rollbacks = 0; 32 | db.addListener("rollback", function () { 33 | ++rollbacks; 34 | }); 35 | 36 | // Both forms take an optional second parameter which is values to 37 | // bind to fields in the query, as an array: 38 | 39 | db.query("INSERT INTO foo (a,b,c) VALUES (?,?,?)", ['apple','banana',22]); 40 | 41 | // or as a map: 42 | 43 | db.query("INSERT INTO bar (x,y,z) VALUES ($x,$y,$zebra)", 44 | {$x: 10, $y:20, $zebra:"stripes"}); 45 | 46 | // Also optional is a callback function which is called with an object 47 | // representing the results of the query: 48 | 49 | db.query("SELECT x FROM bar", function (records) { 50 | assert(records.length == 1); 51 | assert(records[0].x == 10); 52 | 53 | // The HTML5 semantics for the record set also work: 54 | 55 | assert(records.rows.length == 1); 56 | assert(records.rows.item(0).x == 10); 57 | }); 58 | 59 | // INSERT, UPDATE & DELETE queries set `rowsAffected` on their result 60 | // set object: 61 | 62 | db.query("UPDATE foo SET a = ? WHERE a = ?", ['orange', 'apple'], function(r) { 63 | assert(r.rowsAffected == 1); 64 | }); 65 | 66 | // They also emit an `"update"` event. 67 | 68 | // INSERT queries set `insertId`: 69 | 70 | var insert = db.query("INSERT INTO foo VALUES (1,2,3)"); 71 | assert(insert.insertId == 2); 72 | 73 | // Note here that the result set passed to the callback is also 74 | // returned by `query`. 75 | 76 | // Multiple-statement queries are supported; each statement's result set is retuned to the callback as a separate parameter: 77 | 78 | var q = db.query("UPDATE bar SET z=20; SELECT SUM(z) FROM bar;", 79 | function (update, select) { 80 | assert(update.rowsAffected == 1); 81 | assert(select[0]['SUM(z)'] == 20); 82 | }); 83 | 84 | // An array of all result sets is available as the `.all` property on 85 | // each result set: 86 | 87 | assert(q.all[1].length == 1); 88 | 89 | // HTML5 semantics are supported. 90 | 91 | db.transaction(function(tx) { 92 | tx.executeSql("SELECT * FROM foo WHERE c = ?", [3], function(tx,res) { 93 | assert(res.rows.item(0).c == 3); 94 | }); 95 | }); 96 | 97 | // The `query` and `transaction` APIs wrap lower level APIs that more 98 | // thinly wrap the underlying C api: 99 | 100 | var stmt = db.prepare("INSERT INTO foo VALUES (?,?,?)"); 101 | stmt.bind(1, "curly"); 102 | stmt.bind(2, "moe"); 103 | stmt.bind(3, "larry"); 104 | stmt.step(); // Insert Curly, Moe & Larry 105 | stmt.reset(); 106 | stmt.step(); // Insert another row with same stooges 107 | stmt.reset(); 108 | stmt.clearBindings(); 109 | stmt.bind(2, "lonely"); 110 | stmt.step(); // Insert (null, "lonely", null) 111 | stmt.finalize(); 112 | 113 | // Close it: 114 | 115 | db.close(); 116 | 117 | // !!** 118 | // Might as well clean up the mess we made. 119 | 120 | require('fs').unlink('example.db'); 121 | 122 | var sys = require("sys"); 123 | sys.puts("OK"); 124 | -------------------------------------------------------------------------------- /doc/index.php: -------------------------------------------------------------------------------- 1 |
2 |34 | The semantics conform somewhat to those of the HTML5 Web SQL API, 36 | plus some extensions. Also, only the synchronous API is implemented; 37 | the asynchronous API is a big TODO item. 38 | 39 |
41 | The spiritual home of node-sqlite is at 42 | 43 | http://grumdrig.github.com/node-sqlite/. 44 | 45 |
46 | The code lives at 47 | http://github.com/grumdrig/node-sqlite 48 | 49 | readfile('examples.html') ?> 50 | 51 | 52 |
55 |
60 |
63 | $ git clone git://github.com/grumdrig/node-sqlite.git 64 |65 | 66 |
69 | $ cd node-sqlite 70 | $ node-waf configure 71 | $ node-waf build 72 |73 | 74 |
77 | $ node test.js 78 | $ node doc/examples.js 79 |80 | 81 |
84 | The two files needed to use this library are sqlite.js
and
85 | build/default/sqlite3_bindings.node
; copy them where you need
86 | them.
--------------------------------------------------------------------------------
/doc/publish.sh:
--------------------------------------------------------------------------------
1 | docbyex -s examples.js > examples.html
2 | php < index.php > ../index.html
3 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
34 | The semantics conform somewhat to those of the HTML5 Web SQL API, 36 | plus some extensions. Also, only the synchronous API is implemented; 37 | the asynchronous API is a big TODO item. 38 | 39 |
41 | The spiritual home of node-sqlite is at 42 | 43 | http://grumdrig.github.com/node-sqlite/. 44 | 45 |
46 | The code lives at 47 | http://github.com/grumdrig/node-sqlite 48 | 49 | 50 | 72 | 73 | 74 |
75 |
77 | Import the library and open a database. (Only syncronous database 78 | access is implemented at this time.) 79 |
80 |
81 | var sqlite = require("../sqlite"); 82 | var db = sqlite.openDatabaseSync("example.db"); 83 | var assert = require("assert").ok; 84 |85 |
86 | Perform an SQL query on the database: 87 |
88 |
89 | db.query("CREATE TABLE foo (a,b,c)"); 90 |91 |
92 | This is a more convenient form than the HTML5 syntax for the same 93 | thing, but which is also supported: 94 |
95 |
96 | db.transaction(function(tx) { 97 | tx.executeSql("CREATE TABLE bar (x,y,z)"); 98 | }); 99 |100 |
101 | This allows the same or similar code to work on the client and 102 | server end (modulo browser support of HTML5 Web SQL). 103 |
104 | Transactions generate either a "commit" or "rollback" event. 105 |
106 |
107 | var rollbacks = 0; 108 | db.addListener("rollback", function () { 109 | ++rollbacks; 110 | }); 111 |112 |
113 | Both forms take an optional second parameter which is values to 114 | bind to fields in the query, as an array: 115 |
116 |
117 | db.query("INSERT INTO foo (a,b,c) VALUES (?,?,?)", ['apple','banana',22]); 118 |119 |
120 | or as a map: 121 |
122 |
123 | db.query("INSERT INTO bar (x,y,z) VALUES ($x,$y,$zebra)", 124 | {$x: 10, $y:20, $zebra:"stripes"}); 125 |126 |
127 | Also optional is a callback function which is called with an object 128 | representing the results of the query: 129 |
130 |
131 | db.query("SELECT x FROM bar", function (records) { 132 | assert(records.length == 1); 133 | assert(records[0].x == 10); 134 |135 |
136 | The HTML5 semantics for the record set also work: 137 |
138 |
139 | assert(records.rows.length == 1); 140 | assert(records.rows.item(0).x == 10); 141 | }); 142 |143 |
144 | INSERT, UPDATE & DELETE queries set rowsAffected
on their result
145 | set object:
146 |
147 |
148 | db.query("UPDATE foo SET a = ? WHERE a = ?", ['orange', 'apple'], function(r) { 149 | assert(r.rowsAffected == 1); 150 | }); 151 |152 |
153 | They also emit an "update"
event.
154 |
155 | INSERT queries set insertId
:
156 |
157 |
158 | var insert = db.query("INSERT INTO foo VALUES (1,2,3)"); 159 | assert(insert.insertId == 2); 160 |161 |
162 | Note here that the result set passed to the callback is also
163 | returned by query
.
164 |
165 | Multiple-statement queries are supported; each statement's result set is retuned to the callback as a separate parameter: 166 |
167 |
168 | var q = db.query("UPDATE bar SET z=20; SELECT SUM(z) FROM bar;", 169 | function (update, select) { 170 | assert(update.rowsAffected == 1); 171 | assert(select[0]['SUM(z)'] == 20); 172 | }); 173 |174 |
175 | An array of all result sets is available as the .all
property on
176 | each result set:
177 |
178 |
179 | assert(q.all[1].length == 1); 180 |181 |
182 | HTML5 semantics are supported. 183 |
184 |
185 | db.transaction(function(tx) { 186 | tx.executeSql("SELECT * FROM foo WHERE c = ?", [3], function(tx,res) { 187 | assert(res.rows.item(0).c == 3); 188 | }); 189 | }); 190 |191 |
192 | The query
and transaction
APIs wrap lower level APIs that more
193 | thinly wrap the underlying C api:
194 |
195 |
196 | var stmt = db.prepare("INSERT INTO foo VALUES (?,?,?)"); 197 | stmt.bind(1, "curly"); 198 | stmt.bind(2, "moe"); 199 | stmt.bind(3, "larry"); 200 | stmt.step(); // Insert Curly, Moe & Larry 201 | stmt.reset(); 202 | stmt.step(); // Insert another row with same stooges 203 | stmt.reset(); 204 | stmt.clearBindings(); 205 | stmt.bind(2, "lonely"); 206 | stmt.step(); // Insert (null, "lonely", null) 207 | stmt.finalize(); 208 |209 |
210 | Close it: 211 |
212 |
213 | db.close(); 214 |215 |
216 |
217 | 218 |
219 | 220 | 221 | 222 |
225 |
230 |
233 | $ git clone git://github.com/grumdrig/node-sqlite.git 234 |235 | 236 |
239 | $ cd node-sqlite 240 | $ node-waf configure 241 | $ node-waf build 242 |243 | 244 |
247 | $ node test.js 248 | $ node doc/examples.js 249 |250 | 251 |
254 | The two files needed to use this library are sqlite.js
and
255 | build/default/sqlite3_bindings.node
; copy them where you need
256 | them.
--------------------------------------------------------------------------------
/speedtest.js:
--------------------------------------------------------------------------------
1 | // Speed test for SQL. Based on http://www.sqlite.org/speed.html
2 |
3 | var util = require("util");
4 | var posix = require("posix");
5 |
6 | try {
7 | posix.unlink("speedtest.db").wait();
8 | } catch (e) {
9 | // Not there? That's okay.
10 | }
11 |
12 | function connect() {
13 | if (true) {
14 | // node-sqlite
15 | var sqlite = require("./sqlite");
16 | var db = sqlite.openDatabaseSync("speedtest.db");
17 | } else {
18 | // node-persistence
19 | // TODO: this doesn't work yet
20 | var sqlite = require("../../lib/node-persistence/lib/persistence/sqlite");
21 | var db = sqlite.new_connection("speedtest.db");
22 | db.executeSql = db.query;
23 | db.transaction = function(callback) {
24 | this.query("BEGIN TRANSACTION;");
25 | callback(this);
26 | this.query("COMMIT");
27 | }
28 | }
29 | return db;
30 | }
31 |
32 | function _rome(dn, ds) {
33 | if (n < dn) return false;
34 | n -= dn;
35 | s += ds;
36 | return true;
37 | }
38 |
39 | function toRoman(n) {
40 | if (isNaN(n)) return n;
41 | if (n <= 0) return "N";
42 | s = "";
43 | while (_rome(1000, "M")) {}
44 | _rome(900, "CM");
45 | _rome(500, "D");
46 | _rome(400, "CD");
47 | while (_rome(100, "C")) {}
48 | _rome(90, "XC");
49 | _rome(50, "L");
50 | _rome(40, "XL");
51 | while (_rome(10, "X")) {}
52 | _rome(9, "IX");
53 | _rome(5, "V");
54 | _rome(4, "IV");
55 | while (_rome(1, "I")) {}
56 | return s;
57 | }
58 |
59 | var db;
60 |
61 | var SECTION;
62 | var LASTTIME;
63 | function time(section) {
64 | if (db) db.close();
65 | db = connect();
66 | var now = (new Date()).getTime();
67 | if (SECTION) {
68 | var elapsed = ((now - LASTTIME)/1000.0).toFixed(3);
69 | util.puts(elapsed + ' ' + SECTION);
70 | }
71 | SECTION = section;
72 | LASTTIME = now;
73 | }
74 |
75 | time("Test 1: 1000 INSERTs");
76 | db.query("CREATE TABLE t1(a INTEGER, b INTEGER, c VARCHAR(100));");
77 | for (var i = 0; i < 1000; ++i) {
78 | var n = Math.floor(Math.random() * 99999);
79 | db.query("INSERT INTO t1 VALUES(?,?,?);", [i, n, toRoman(n)]);
80 | }
81 |
82 | time("Test 2: 25000 INSERTs in a transaction");
83 | db.transaction(function (tx) {
84 | tx.executeSql("CREATE TABLE t2(a INTEGER, b INTEGER, c VARCHAR(100));");
85 | for (var i = 0; i < 25000; ++i) {
86 | var n = Math.floor(Math.random() * 99999);
87 | db.query("INSERT INTO t2 VALUES(?,?,?);", [i, n, toRoman(n)]);
88 | }
89 | });
90 |
91 | time("Test 3: 25000 INSERTs into an indexed table");
92 | db.transaction(function (tx) {
93 | tx.executeSql("CREATE TABLE t3(a INTEGER, b INTEGER, c VARCHAR(100));");
94 | tx.executeSql("CREATE INDEX i3 ON t3(c);");
95 | for (var i = 0; i < 25000; ++i) {
96 | var n = Math.floor(Math.random() * 99999);
97 | db.query("INSERT INTO t3 VALUES(?,?,?);", [i, n, toRoman(n)]);
98 | }
99 | });
100 |
101 | time("Test 4: 100 SELECTs without an index");
102 | db.transaction(function (tx) {
103 | for (var i = 0; i < 100; ++i) {
104 | db.query("SELECT count(*), avg(b) FROM t2 WHERE b>=? AND b;",
105 | [i * 100, i * 100 + 1000]);
106 | }
107 | });
108 |
109 | time("Test 5: 100 SELECTs on a string comparison");
110 | db.transaction(function (tx) {
111 | for (var i = 0; i < 100; ++i) {
112 | db.query("SELECT count(*), avg(b) FROM t2 WHERE c LIKE ?;",
113 | ['%' + toRoman(i) + '%']);
114 | }
115 | });
116 |
117 | time("Test 6: Creating an index");
118 | db.query("CREATE INDEX i2a ON t2(a);");
119 | db.query("CREATE INDEX i2b ON t2(b);");
120 |
121 | time("Test 7: 5000 SELECTs with an index");
122 | for (var i = 0; i < 5000; ++i)
123 | db.query("SELECT count(*), avg(b) FROM t2 WHERE b>=? AND b;",
124 | [i * 100, (i+1) * 100]);
125 |
126 |
127 | time("Test 8: 1000 UPDATEs without an index");
128 | db.transaction(function (tx) {
129 | for (var i = 0; i < 1000; ++i)
130 | tx.executeSql("UPDATE t1 SET b=b*2 WHERE a>=? AND a",
131 | [i * 10, (i+1) * 10]);
132 | });
133 |
134 | time("Test 9: 25000 UPDATEs with an index");
135 | db.transaction(function (tx) {
136 | for (var i = 0; i < 25000; ++i) {
137 | var n = Math.floor(Math.random() * 1000000);
138 | tx.executeSql("UPDATE t2 SET b=? WHERE a=?", [n, i]);
139 | }
140 | });
141 |
142 | time("Test 10: 25000 text UPDATEs with an index");
143 | db.transaction(function (tx) {
144 | for (var i = 0; i < 25000; ++i) {
145 | var n = Math.floor(Math.random() * 1000000);
146 | tx.executeSql("UPDATE t2 SET c=? WHERE a=?", [toRoman(n), i]);
147 | }
148 | });
149 |
150 | time("Test 11: INSERTs from a SELECT");
151 | db.transaction(function (tx) {
152 | tx.executeSql("INSERT INTO t1 SELECT b,a,c FROM t2;");
153 | tx.executeSql("INSERT INTO t2 SELECT b,a,c FROM t1;");
154 | });
155 |
156 | time("Test 12: DELETE without an index");
157 | db.query("DELETE FROM t2 WHERE c LIKE '%fifty%';");
158 |
159 | time("Test 13: DELETE with an index");
160 | db.query("DELETE FROM t2 WHERE a>10 AND a<20000;");
161 |
162 | time("Test 14: A big INSERT after a big DELETE");
163 | db.query("INSERT INTO t2 SELECT * FROM t1;");
164 |
165 | time("Test 15: A big DELETE followed by 12000 small INSERTs");
166 | db.transaction(function (tx) {
167 | tx.executeSql("DELETE FROM t1;");
168 | for (var i = 0; i < 12000; ++i) {
169 | var n = Math.floor(Math.random() * 100000);
170 | tx.executeSql("INSERT INTO t1 VALUES(?,?,?);", [i, n, toRoman(n)]);
171 | }
172 | });
173 |
174 | time("Test 16: DROP TABLE");
175 | db.query("DROP TABLE t1;");
176 | db.query("DROP TABLE t2;");
177 | db.query("DROP TABLE t3;");
178 |
179 | time();
--------------------------------------------------------------------------------
/sqlite.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2009, Eric Fredricksen