    Version 2.0.2

onecrayon (author)
July 11, 2011
commit 41d2edf75bd094de974e
tree ca0eddc77a2251f4ab53
parent e6399fa35d3df0f70978

- Edit this file

100644 843 lines (786 sloc) 25.152 kb

- raw
- blame
- history

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    829
    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843

    /*---
    script: database.js
    description: Provides an interface to HTML 5 database objects for WebOS using an Enyo component
    license: MIT license <http://www.opensource.org/licenses/mit-license.php>
    authors:- Ian Beck
    version: 2.0.2
    Core class design based on the Mootools Database class by André Fiedler:http://github.com/SunboX/mootools-database/
    Schema definitions based on Mojo Database Helper Objects by Dave Freeman:http://webos101.com/Mojo_Database_Helper_Objects
    ...*/
    /** * onecrayon.Database (component) * * This is the component you'll be using in your own code. Provides shortcuts * to common HTML 5 SQLite database operations. * * Parameters: * - database (string, required): name of your database; prefix with ext: to allow >1 MB sizes * - version (string): version of the database you want to open/create * - estimatedSize (int): estimated size in bytes * - debug (bool): if true, outputs verbose debugging messages (mainly SQL that's being run) * * USAGE: * components: [ *     { *         name: "myDatabase", kind: "onecrayon.Database", *         database: 'ext:my_database', *         version: '1', estimatedSize: 1048576, debug: true *     } * ] */enyo.kind({	name: "onecrayon.Database",	kind: enyo.Component,	published: {		database: '',		version: '1',		estimatedSize: null,		debug: false	},
    	/** @protected */	db: undefined,	dbVersion: null,	lastInsertRowId: 0,
    	// === Constructor and creation logic ===
    	/** @protected */	constructor: function() {		// Run our default construction		this.inherited(arguments);
    		// Setup listing of bound methods		// Cuts down on memory usage spikes, since bind() creates a new method every call, but causes more initial memory to be allocated		this.bound = {			setSchema: enyo.bind(this, this.setSchema),			insertData: enyo.bind(this, this.insertData),			_errorHandler: enyo.bind(this, this._errorHandler)		};
    		// @deprecated methods and properties; provided for backwards compatibility		this.lastInsertID = this.lastInsertId;		this.setSchemaFromURL = this.setSchemaFromUrl;		this.insertDataFromURL = this.insertDataFromUrl;		this.changeVersionWithSchemaFromURL = this.changeVersionWithSchemaFromUrl;	},
    	/** @protected */	create: function() {		this.inherited(arguments);
    		// Database name is required; enforce it		if (this.database === '') {			this.error('Database: you must define a name for your database when instantiating the kind using the `database` property.');			return null;		}		// Just to make sure people are aware...		if (this.database.indexOf('ext:') !== 0) {			this.warn('Database: you are working with an internal database, which will limit its size to 1 MB. Prepend `ext:` to your database name to remove this restriction.');		}		// Open our database connection		// parameters: name, version, displayName [unused in WebOS], target size		this.db = openDatabase(this.database, this.version, '', this.estimatedSize);		// Make sure everything is peachy		if (!this.db) {			this.error('Database: failed to open database named ' + this.database);			return null;		}		// Save the database version, in case it differs from options		this.dbVersion = this.db.version;	},
    	// === Standard database methods ===
    	/**	 * Fetch the version of the database	 */	getVersion: function() {		return this.dbVersion;	},
    	/**	 * Exposes the last ID inserted	 */	lastInsertId: function() {		return this.lastInsertRowId;	},
    	/**	 * Close the database connection	 *	 * Why you'd want to do this, I don't know; may as well support it, though	 */	close: function() {		this.db.close();	},
    	/**	 * Destroy the entire database for the given version (if passed)	 *	 * If only there were a way to actually do this...	 */	remove: function(version) {		this.error('Database: there is currently no way to destroy a database. Hopefully we will be able to add this in the future.');	},
    	/**	 * Execute an arbitrary SQL command on the database.	 *	 * If you need to execute multiple commands in a transaction, use queries()	 *	 * Parameters:	 * - sql (string or query object, required)	 * - options (object):	 *    * values (array): replacements for '?' placeholders in SQL	 *      (only use if not passing a DatabaseQuery object)	 *    * onSuccess (function): method to call on successful query	 *        + receives single argument: results as an array of objects	 *    * onError (function): method to call on error; defaults to logging	 */	query: function(sql, options) {		// Possible that the user closed the connection already, so double check		if (!this.db) {			this._db_lost();			return;		}		// Merge in user options (if any) to defaults		var options = (typeof options !== 'undefined' ? options : {});		// Check to see if they passed in a query object		if (!enyo.isString(sql)) {			// Translate into options object and SQL string			options.values = sql.values;			sql = sql.sql;		}		// Run the actual merge for our options, making sure there's a default values array		options = this._getOptions(options, {"values": []});		// Trim whitespace to make sure we can accurately check character positions		sql = enyo.string.trim(sql);		if (sql.lastIndexOf(';') !== sql.length - 1) {			sql = sql + ';';		}		// Run the transaction		var self = this;		this.db.transaction(function(transaction) {			if (self.debug) {				// Output the query to the log for debugging				self.log(sql, ' ==> ', options.values);			}			transaction.executeSql(sql, options.values, function(transaction, results) {				// We use this anonymous function to format the results				// Just passing the SQLResultSet object would require SQLite-specific code on the part of the callback
    				// Try to snag the last insert ID, if available				try {					self.lastInsertRowId = results.insertId;				} catch(e) {}				// Call the onSuccess with formatted results				if (options.onSuccess) {					options.onSuccess(self._convertResultSet(results));				}			}, options.onError);		});	},
    	/**	 * Execute multiple arbitrary SQL queries on the database as a single	 * transaction (group of inserts, for instance)	 *	 * Notes:	 * - Not appropriate for SELECT or anything with returned rows	 * - The last inserted ID will NOT be set when using this method	 * - onSuccess and onError are only for the transaction! NOT individual queries	 *	 * Parameters:	 * - queries (array, required):	 *    * SQL strings or DatabaseQuery objects	 * - options (object):	 *    * onSuccess: function to execute on LAST QUERY success	 *    * onError: function to execute on TRANSACTION error	 */	queries: function(queries, options) {		// Possible that the user closed the connection already, so double check		if (!this.db) {			this._db_lost();			return;		}		// Merge in user options (if any) to defaults		var options = (typeof options !== 'undefined' ? options : {});		options = this._getOptions(options);		// Run the transaction		var DEBUG = this.debug;		var self = this;		this.db.transaction(function(transaction) {			// Loop over each query and execute it			var length = queries.length;			var query = null;			// Init variables for tracking SQL and values			var sql = '';			var values = [];			for (var i = 0; i < length; i++) {				query = queries[i];				// If query isn't a string, it's an object				if (enyo.isString(query)) {					sql = query;				} else {					sql = query.sql;					values = query.values;				}				if (DEBUG) {					// Output query to the log for debugging					self.log(sql, " ==> ", values);				}				if (i === length - 1) {					// Last call					transaction.executeSql(sql, values, options.onSuccess);				} else {					transaction.executeSql(sql, values);				}			}		}, options.onError);	},

    	// === JSON methods ===
    	/**	 * A core goal of the Database class is to enable you to easily port data	 * into your database using JSON.	 *	 * setSchema defines/inserts a table layout (if it doesn't already exist)	 * and inserts any data that you've provided inline	 *	 * Parameters:	 * - schema (object): see advanced description below	 * - options (object):	 *    * onSuccess (function): called after successful transactions	 *    * onError (function): called on error for transactions	 *	 * PLEASE NOTE: the onSuccess and onError functions may be called multiple	 * times if you are inserting data as well as defining a table schema.	 * 	 * Schema Description	 * ==================	 *	 * An array of table objects, which each contain an array of columns objects	 * and an optional array of data to insert	 * 	 * Array of table objects (optional if single table) =>	 *     table Object =>	 *         table (text, required; name of the table)	 *         columns (array) =>	 *             column (text, required; name of the column)	 *             type (text, required)	 *             constraints (array of strings)	 *         data (array) =>	 *             Object (keys are the names of the columns)	 *     string (executed as a straight SQL query)	 *	 * Both columns and data are optionally; you can use setSchema to	 * define the table schema, populate with data, or both.	 *	 * Obviously, it's better practice to populate with data only when you	 * need to, whereas you'll likely be defining tables every time you	 * instantiate the Database class.	 *	 * You may also use an SQL string instead of a table object if you desire.	 * This is useful for running batch updates to modify existing schema, for	 * instance, as you can mix and match new tables with ALTER TABLE statements.	 *	 * JSON example	 * ============	 *	 * [	 *     {	 *         "table": "table1",	 *         "columns": [	 *             {	 *                 "column": "entry_id",	 *                 "type": "INTEGER",	 *                 "constraints": ["PRIMARY_KEY"]	 *             },	 *             {	 *                 "column": "title",	 *                 "type": "TEXT"	 *             }	 *         ],	 *         "data": [	 *             { "entry_id": "1", "title": "My first entry" },	 *             { "entry_id": "2", "title": "My second entry" }	 *         ]	 *     },	 *     "ALTER TABLE table1 ADD COLUMN category TEXT"	 * ]	 */	setSchema: function(schema, options) {		// Check to see if it's a single table, make array for convenience		if (!enyo.isArray(schema)) {			schema = [schema];		}		// Merge in user options (if any) to defaults		var options = (typeof options !== 'undefined' ? options : {});		options = this._getOptions(options);		// Setup array to track table creation SQL		var tableQueries = [];		// Setup array to track data (just in case)		var data = [];		// Loop over the tables		var length = schema.length;		var table = null;		for (var i = 0; i < length; i++) {			table = schema[i];			// Check to see if we have an SQL string			if (enyo.isString(table)) {				tableQueries.push(table);			} else {				// Check for and save columns object				if (typeof table.columns !== 'undefined') {					tableQueries.push(this.getCreateTable(table.table, table.columns));				}				// Check for and save data array				if (typeof table.data !== 'undefined') {					data.push({"table": table.table, "data": table.data});				}			}		}		if (data.length > 0) {			var dataInsertFollowup = enyo.bind(this, this.insertData, data, options);			// Execute the queries			this.queries(tableQueries, {				onSuccess: dataInsertFollowup,				onError: options.onError			});		} else {			this.queries(tableQueries, options);		}	},

    	/**	 * Allows you to set your schema using an arbitrary JSON file.	 *	 * Parameters:	 *     - url (string, required): local or remote URL for JSON file	 *     - options (object): same as setSchema options (above)	 */	setSchemaFromUrl: function(url, options) {		this._readUrl(url, this.bound.setSchema, options);	},
    	/**	 * Inserts arbitrary data from a Javascript object	 *	 * Parameters:	 * - data (array or object):	 *     * table (string, required): name of the table to insert into	 *     * data (array, required): array of objects whose keys are the column	 *       names to insert into	 * - options (object):	 *     * onSuccess (function): success callback	 *     * onError (function): error callback	 *	 * The formatting is the same as for the schema, just without the columns.	 * Note that data can be a single object if only inserting into one table.	 */	insertData: function(data, options) {		// Check to see if it's a single table		if (!enyo.isArray(data)) {			data = [data];		}		// Merge in user options (if any) to defaults		var options = (typeof options !== 'undefined' ? options : {});		options = this._getOptions(options);		// Setup array to track queries		var dataQueries = [];		var length = data.length;		var table = null;		var i, j;		var insertsLength = 0;		var row = null;		for (i = 0; i < length; i++) {			table = data[i];			// Make sure there's actually a data array			if (typeof table.data !== 'undefined') {				var tableName = table.table;				// Check to see if we have more than one row of data				var inserts = null;				if (!enyo.isArray(table.data)) {					inserts = [table.data]				} else {					inserts = table.data;				}				// Nested loop to fetch the data inserts				insertsLength = inserts.length;				for (j = 0; j < insertsLength; j++) {					row = inserts[j];					dataQueries.push(this.getInsert(tableName, row));				}			}		}		// Execute that sucker!		this.queries(dataQueries, options);	},
    	/**	 * Allows you to populate data using arbitrary JSON file.	 *	 * Parameters:	 * - url (string, required): local or remote URL for JSON file	 * - options (object): same as insertData options (above)	 */	insertDataFromUrl: function(url, options) {		this._readUrl(url, this.bound.insertData, options);	},

    	// === VERSIONING METHODS ===
    	/**	 * Change the version of the database; allows porting data when	 * upgrading schema	 *	 * WARNING: you must have NO other database connections active when you	 * do this, and remember that afterward you will need to use the new	 * version in your `new Database()` calls.	 */	changeVersion: function(newVersion) {		// Backwards compatibility with previous incarnation which was changeVersion(from, to)		if (arguments.length > 1) {			newVersion = arguments[1];		}		var self = this;		this.db.changeVersion(this.dbVersion, newVersion, function() {}, function() {			if (self.debug) {				self.error("DATABASE VERSION UPDATE FAILED: " + newVersion);			}		}, function() {			if (self.debug) {				self.log("DATABASE VERSION UPDATE SUCCESS: " + newVersion);			}		});		this.dbVersion = newVersion;	},
    	/**	 * Change the version of the database and apply any schema updates	 * specified in the `schema` object	 *	 * NOTE: You cannot insert data with this call. Instead, run your schema	 * update and then use insertData in your success callback	 *	 * Parameters:	 * - newVersion (string or int)	 * - schema (object or string): same as setSchema (documented above),	 *   minus any data insertion support	 * - options (object): same as setSchema options	 */	changeVersionWithSchema: function(newVersion, schema, options) {		// Check to see if it's a single table, make array for convenience		if (!enyo.isArray(schema)) {			schema = [schema];		}		// Merge in user options (if any) to defaults		var options = (typeof options !== 'undefined' ? options : {});		options = this._getOptions(options);
    		// Run the changeVersion update!		this.db.changeVersion(this.dbVersion, newVersion, enyo.bind(this, function(transaction) {			// Loop over the items in the schema			var length = schema.length;			var item = null, query = null, sql = null, values = null;			for (var i = 0; i < length; i++) {				item = schema[i];				// Check to see if we have an SQL string or table definition				if (enyo.isString(item)) {					query = item;				} else if (typeof item.columns !== 'undefined') {					query = this.getCreateTable(item.table, item.columns);				}
    				// Run the query				sql = (enyo.isString(query) ? query : query.sql);				values = (typeof query.values !== 'undefined' ? query.values : null);				if (this.debug) {					// Output the query to the log for debugging					this.log(sql, ' ==> ', values);				}				if (values !== null) {					transaction.executeSql(sql, values);				} else {					transaction.executeSql(sql);				}			}		}), options.onError, enyo.bind(this, this._versionChanged, newVersion, options.onSuccess));	},
    	/**	 * Change the version of the database and apply any schema updates	 * specified in the schema JSON file located at `url`	 */	changeVersionWithSchemaFromUrl: function(newVersion, url, options) {		this._readUrl(url, enyo.bind(this, this.changeVersionWithSchema, newVersion));	},

    	// === SQL Methods ===
    	/**	 * SQL to Insert records (create)	 *	 * Parameters:	 * - tableName (string, required)	 * - data (object, required):	 *     * key: value pairs to be updated as column: value (same format as data	 *       objects in schema)	 *	 * Returns DatabaseQuery object	 */	getInsert: function(tableName, data) {		var sql = 'INSERT INTO ' + tableName + ' (';		var valueString = ' VALUES (';		// Set up our tracker array for value placeholders		var colValues = [];		// Loop over the keys in our object		for (var key in data) {			// Add the value to the valueString			colValues.push(data[key]);			// Add the placeholders			sql += key;			valueString += '?';			// Append commas			sql += ', ';			valueString += ', ';		}		// Remove extra commas and insert closing parentheses		sql = sql.substr(0, sql.length - 2) + ')';		valueString = valueString.substr(0, valueString.length - 2) + ')';		// Put together the full SQL statement		sql += valueString;		// At long last, we've got our SQL; return it		return new onecrayon.DatabaseQuery({'sql': sql, 'values': colValues});	},
    	/**	 * SQL for a very simple select	 *	 * Parameters:	 * - tableName (string, required)	 * - columns (string, array, or null): names of the columns to return	 * - where (object): {key: value} is equated to column: value	 *	 * Returns DatabaseQuery object	 */	getSelect: function(tableName, columns, where) {		var sql = 'SELECT ';		// Setup our targeted columns		var colStr = '';		if (columns === null || columns === '') {			colStr = '*';		} else if (enyo.isArray(columns)) {			// Cut down on memory needs with a straight for loop			var length = columns.length;			var colStr = [];			for (var i = 0; i < length; i++) {				colStr.push(columns[i]);			}			// Join the column string together with commas			colStr = colStr.join(', ');		}		sql += colStr + ' FROM ' + tableName;		// Parse the WHERE object if we have one		if (typeof where !== 'undefined') {			sql += ' WHERE ';			var sqlValues = [];			var whereStrings = [];			// Loop over the where object to populate			for (var key in where) {				sqlValues.push(where[key]);				whereStrings.push(key + ' = ?');			}			// Add the WHERE strings to the sql			sql += whereStrings.join(' AND ');		}		return new onecrayon.DatabaseQuery({'sql': sql, 'values': sqlValues});	},
    	/**	 * SQL to update a particular row	 *	 * Parameters:	 * - tableName (string, required)	 * - data (object, required):	 *     * key: value pairs to be updated as column: value (same format as	 *       data objects in schema)	 * - where (object): key: value translated to 'column = value'	 *	 * Returns DatabaseQuery object	 */	getUpdate: function(tableName, data, where) {		var sql = 'UPDATE ' + tableName + ' SET ';		var sqlValues = [];		var sqlStrings = [];		// Loop over data object		for (var key in data) {			sqlStrings.push(key + ' = ?');			sqlValues.push(data[key]);		}		// Collapse sqlStrings into SQL		sql += sqlStrings.join(', ');		// Parse the WHERE object		sql += ' WHERE ';		var whereStrings = [];		// Loop over the where object to populate		for (var key in where) {			whereStrings.push(key + ' = ?');			sqlValues.push(where[key]);		}		// Add the WHERE strings to the sql		sql += whereStrings.join(' AND ');		return new onecrayon.DatabaseQuery({'sql': sql, 'values': sqlValues});	},
    	/**	 * SQL to delete records	 *	 * Parameters:	 * - tableName (string, required)	 * - where (object, required): key: value mapped to 'column = value'	 *	 * Returns DatabaseQuery object	 */	getDelete: function(tableName, where) {		var sql = 'DELETE FROM ' + tableName + ' WHERE ';		var sqlValues = [];		var whereStrings = [];		// Loop over the where object to populate		for (var key in where) {			whereStrings.push(key + ' = ?');			sqlValues.push(where[key]);		}		// Add the WHERE strings to the sql		sql += whereStrings.join(' AND ');		return new onecrayon.DatabaseQuery({'sql': sql, 'values': sqlValues});	},
    	/**	 * SQL to create a new table	 *	 * Parameters:	 * - tableName (string, required)	 * - columns (array, required): uses syntax from setSchema (see above)	 * - ifNotExists (bool, defaults to true)	 *	 * Returns string, since value substitution isn't supported for this	 * statement in SQLite	 */	getCreateTable: function(tableName, columns, ifNotExists) {		var ifNotExists = (typeof ifNotExists !== 'undefined' ? ifNotExists : true);		// Setup the basic SQL		var sql = 'CREATE TABLE ';		if (ifNotExists) {			sql += 'IF NOT EXISTS ';		}		sql += tableName + ' (';		// Add the column definitions to the SQL		var length = columns.length;		var col = null;		var colStr = [];		var colDef = '';		for (var i = 0; i < length; i++) {			col = columns[i];			// Construct the string for the column definition			colDef = col.column + ' ' + col.type;			if (col.constraints) {				colDef += ' ' + col.constraints.join(' ');			}			// Add to SQL			colStr.push(colDef);		}		sql += colStr.join(', ') + ')';		return sql;	},
    	/**	 * SQL for dropping a table	 *	 * Returns string	 */	getDropTable: function(tableName) {		return 'DROP TABLE IF EXISTS ' + tableName;	},

    	// === Private methods ===
    	/**	 * @protected	 * Sets the local tracking variable for the DB version	 *	 * PRIVATE FUNCTION; use the changeVersion* functions to modify	 * your database's version information	 */	_versionChanged: function(newVersion, callback) {		this.dbVersion = newVersion;		callback();	},
    	/**	 * @protected	 * Merge user options into the standard set	 * 	 * Parameters:	 * - userOptions (object, required): options passed by the user	 * - extraOptions (object, optional) any default options beyond onSuccess	 *   and onError	 */	_getOptions: function(userOptions, extraOptions) {		var opts = {			"onSuccess": this._emptyFunction,			"onError": this.bound._errorHandler		};		if (typeof extraOptions !== 'undefined') {			opts = enyo.mixin(opts, extraOptions);		}		if (typeof userOptions === 'undefined') {			var userOptions = {};		}		return enyo.mixin(opts, userOptions);	},
    	/** @protected */	_emptyFunction: function() {},
    	/**	 * @protected	 * Used to read in external JSON files	 */	_readUrl: function(url, callback, options) {		enyo.xhrGet({			'url': url,			load: enyo.bind(this, function(responseText, response) {				// I have no idea why status can be zero when reading file locally, but it can				if (response.status === 200 || response.status === 0) {					try {						var json = enyo.json.parse(responseText);						callback(json, options);					} catch (e) {						this.error('JSON request error:', e);					}				} else {					this.error('Database: failed to read JSON at URL `' + url + '`');				}			})		});	},
    	/**	 * @protected	 * Converts an SQLResultSet into a standard Javascript array of results	 */	_convertResultSet: function(rs) {		var results = [];		if (rs.rows) {			for (var i = 0; i < rs.rows.length; i++) {				results.push(rs.rows.item(i));			}		}		return results;	},
    	/**	 * @protected	 * Used to report generic database errors	 */	_errorHandler: function(transaction, error) {		// If a transaction error (rather than an executeSQL error) there might only be one parameter		if (typeof error === 'undefined') {			var error = transaction;		}		this.error('Database error (' + error.code + '): ' + error.message);	},
    	/**	 * @protected	 * Used to output "database lost" error	 */	_db_lost: function() {		this.error('Database: connection has been closed or lost; cannot execute SQL');	}});
    /** * onecrayon.DatabaseQuery (object) * * This is a helper  that, at the moment, is basically just an object * with standard properties. * * Maybe down the road I'll add some helper methods for working with queries. * * USAGE: * var myQuery = new onecrayon.DatabaseQuery({ *     sql: 'SELECT * FROM somewhere WHERE id = ?', *     values: ['someID'] * }); */onecrayon.DatabaseQuery = function(inProps) {	this.sql = (typeof inProps.sql !== 'undefined' ? inProps.sql : '');	this.values = (typeof inProps.values !== 'undefined' ? inProps.values : []);};

## Keyboard Shortcuts (see all)

### Site wide shortcuts

s
  Focus site search

?
  Bring up this help dialog

### Commit list

j
  Move selected down

k
  Move selected up

t
  Open tree

p
  Open parent

c or o or enter
  Open commit

y
  Expand URL to its canonical form

### Pull request list

j
  Move selected down

k
  Move selected up

o or enter
  Open issue

### Issues

j
  Move selected down

k
  Move selected up

x
  Toggle select target

o or enter
  Open issue

I
  Mark selected as read

U
  Mark selected as unread

e
  Close selected

y
  Remove selected from view

c
  Create issue

l
  Create label

i
  Back to inbox

u
  Back to issues

/
  Focus issues search

### Network Graph

← or h
  Scroll left

→ or l
  Scroll right

↑ or k
  Scroll up

↓ or j
  Scroll down

t
  Toggle visibility of head labels

shift ← or shift h
  Scroll all the way left

shift → or shift l
  Scroll all the way right

shift ↑ or shift k
  Scroll all the way up

shift ↓ or shift j
  Scroll all the way down

### Source Code Browsing

t
  Activates the file finder

l
  Jump to line

y
  Expand URL to its canonical form

## Markdown Cheat Sheet

### Format Text

Headers

    # This is an <h1> tag
    ## This is an <h2> tag
    ###### This is an <h6> tag

Text styles

    *This text will be italic*
    _This will also be italic_
    **This text will be bold**
    __This will also be bold__

    *You **can** combine them*

### Lists

Unordered

    * Item 1
    * Item 2
      * Item 2a
      * Item 2b

Ordered

    1. Item 1
    2. Item 2
    3. Item 3
       * Item 3a
       * Item 3b

### Miscellaneous

Images

    ![GitHub Logo](/images/logo.png)
    Format: ![Alt Text](url)

Links

    http://github.com - automatic!
    [GitHub](http://github.com)

Blockquotes

    As Kanye West said:
    > We're living the future so
    > the present is our past.

### Code Examples in Markdown

Syntax highlighting with GFM

    ```javascript
    function fancyAlert(arg) {
      if(arg) {
        $.facebox({div:'#foo'})
      }
    }
    ```

Or, indent your code 4 spaces

    Here is a Python code example
    without syntax highlighting:

        def foo:
          if not bar:
            return true

Inline code for comments

    I think you should use an
    `<addr>` element here instead.