Db.Model = new Class ().extend ({ Status: { CLEAN : 1 ,LOADING : 2 ,READY : 3 ,ERROR : 4 } }); Db.Model.implement ({ Extends: Vn.Object ,Tag: 'db-model' ,Child: 'query' ,Properties: { /** * The connection used to execute the statement. **/ conn: { type: Db.Conn ,set: function (x) { this._conn = x; this.refresh (); } ,get: function () { return this._conn; } }, /** * The result index. **/ resultIndex: { type: Number ,set: function (x) { this._resultIndex = x; } ,get: function () { return this._resultIndex; } }, /** * The batch used to execute the statement. **/ batch: { type: Sql.Batch ,set: function (x) { this.link ({_batch: x}, {'changed': this.refresh}); this.refresh (); } ,get: function () { return this._batch; } }, /** * The model select statement. **/ stmt: { type: Sql.Stmt ,set: function (x) { this._stmt = x; this.refresh (); } ,get: function () { return this._stmt; } }, /** * The model query. **/ query: { type: String ,set: function (x) { this._stmt = new Sql.String ({query: x}); } ,get: function () { return this._stmt.render (null); } }, /** * The main table. **/ mainTable: { type: String ,set: function (x) { this._mainTable = null; this.requestedMainTable = x; this.refreshMainTable (); } ,get: function () { return this._mainTable; } }, /** * Determines if the model is updatable. **/ updatable: { type: Boolean ,set: function (x) { this._updatable = false; this.requestedUpdatable = x; this.refreshUpdatable (); } ,get: function () { return this._updatable; } }, /** * The number of rows in the model. **/ numRows: { type: Number ,get: function () { if (this.data) return this.data.length; return 0; } }, /** * The current status of the model. **/ status: { type: Number ,get: function () { return this._status; } }, /** * Checks if the model data is ready. **/ ready: { type: Boolean ,get: function () { return this._status == Db.Model.Status.READY; } } } ,_resultIndex: 0 ,_batch: null ,_stmt: null ,_status: Db.Model.Status.CLEAN ,requestedMainTable: null ,requestedUpdatable: true ,data: null ,columns: null ,columnMap: null ,insertedRow: -1 ,defaults: [] ,columnDefaults: [] ,sortColumn: -1 ,requestedIndexes: {} ,indexes: [] ,initialize: function (props) { this.parent (props); this.cleanData (); this.setStatus (Db.Model.Status.CLEAN); } ,loadXml: function (builder, node) { this.parent (builder, node); var query = node.firstChild.nodeValue; if (query) this.query = query; } ,refresh: function () { if (this._stmt && this._batch) this._stmt.findHolders (this._batch); if (this._conn && this._stmt && (!this._batch || this._batch.isReady ())) { this.setStatus (Db.Model.Status.LOADING); this._conn.execStmt (this._stmt, this.selectDone.bind (this), this._batch); } else { this.cleanData (); this.setStatus (Db.Model.Status.CLEAN); } } ,cleanData: function (error) { this.data = null; this.columns = null; this.columnMap = null; this.indexes = []; } ,refreshUpdatable: function () { var oldValue = this._updatable; this._updatable = this._mainTable != null && this.requestedUpdatable; if (oldValue != this._updatable) this.signalEmit ('updatable-changed'); } ,refreshMainTable: function () { var newMainTable = null; var newMainSchema = null; var x = this.columns; if (x) for (var i = 0; i < x.length; i++) if (x[i].flags & Db.Conn.Flag.PRI_KEY) if (!this.requestedMainTable || x[i].table === this.requestedMainTable) { newMainTable = x[i].table; break; } this._mainTable = newMainTable; this.refreshUpdatable (); } /** * Sets the default value for inserted fields. * * @param {String} field The destination field name * @param {String} table The destination table name * @param {Object} value The default value **/ ,setDefaultFromValue: function (field, table, value) { this.defaults.push ({ field: field ,table: table ,value: value }); } /** * Sets the default value for inserted fields from another column in the * model. * * @param {String} field The destination field name * @param {String} table The destination table name * @param {String} srcColumn The source column **/ ,setDefaultFromColumn: function (field, table, srcColumn) { this.columnDefaults.push ({ field: field ,table: table ,srcColumn: srcColumn }); } /** * Checks if the column exists. * * @param {integer} column The column index * @return {Boolean} %true if column exists, %false otherwise **/ ,checkColExists: function (column) { return this.columns && column >= 0 && column < this.columns.length; } /** * Checks if the row exists. * * @param {integer} row The row index * @return {Boolean} %true if row exists, %false otherwise **/ ,checkRowExists: function (row) { return this.data && row >= 0 && row < this.data.length; } ,checkRowUpdatable: function (row) { return this.checkRowExists (row); } /** * Get the index of the column from its name. * * @param {string} columnName The column name * @return {number} The column index or -1 if column not exists **/ ,getColumnIndex: function (columnName) { var index; if (this.columnMap && (index = this.columnMap[columnName]) !== undefined) return index; return -1; } /** * Gets a value from the model. * * @param {number} row The row index * @param {string} columnName The column name * @return {mixed} The value **/ ,get: function (row, columnName) { var index = this.getColumnIndex (columnName); if (index != -1) return this.getByIndex (row, index); return undefined; } /** * Updates a value on the model. * * @param {number} row The row index * @param {string} columnName The column name * @param {mixed} value The new value **/ ,set: function (row, columnName, value) { var index = this.getColumnIndex (columnName); if (index != -1) this.setByIndex (row, index, value); } /** * Gets a value from the model using the column index. * * @param {number} row The row index * @param {number} column The column index * @return {mixed} The value **/ ,getByIndex: function (row, column) { if (this.checkRowExists (row) && this.checkColExists (column)) return this.data[row][column]; return undefined; } /** * Updates a value on the model using the column index. * * @param {number} row The row index * @param {number} col The column index * @param {mixed} value The new value **/ ,setByIndex: function (row, col, value) { if (!(this.checkRowUpdatable (row) && this.checkColExists (col))) return; if (row == this.insertedRow) { this.performUpdate (row, [col], [value]); return; } var column = this.columns[col]; var where = this.getWhere (column.table, row, false); if (where) { var multiStmt = new Sql.MultiStmt (); var table = new Sql.Table ({ name: column.orgtable ,schema: column.db }); var update = new Sql.Update ({where: where}); update.addTarget (table); update.addSet (column.orgname, value); multiStmt.addStmt (update); var select = new Sql.Select ({where: where}); select.addTarget (table); select.addField (column.orgname); multiStmt.addStmt (select); var updateData = { row: row ,columns: [col] }; this._conn.execStmt (multiStmt, this.updateDone.bind (this, updateData)); } else this.sendInsert (column.table, row, col, value); } /** * Deletes a row from the model. * * @param {number} row The row index **/ ,deleteRow: function (row) { if (!this.checkRowUpdatable (row)) return; if (row != this.insertedRow) { var where = this.getWhere (this._mainTable, row, false); if (where) { var deleteQuery = new Sql.Delete ({where: where}); var table = this.getTarget (this._mainTable); deleteQuery.addTarget (table); this._conn.execStmt (deleteQuery, this.deleteDone.bind (this, row)); } } else { this.performDelete (row); this.insertedRow = -1; } } /** * Inserts a new row on the model. * * @return The index of the inserted row **/ ,insertRow: function () { if (!this._updatable || this.insertedRow != -1) return -1; var x = this.columns; var newRow = new Array (x.length); for (var i = 0; i < x.length; i++) if (x[i].table === this._mainTable) newRow[i] = x[i].def; else newRow[i] = null; this.insertedRow = this.data.push (newRow) - 1; this.signalEmit ('row-inserted', this.insertedRow); return this.insertedRow; } ,performOperations: function () { if (this.insertedRow == -1) return; this.sendInsert (this._mainTable, this.insertedRow, -1, null); } /* * Function used to sort the model. */ ,sortFunction: function (column, a, b) { if (a[column] < b[column]) return -1; else if (a[column] > b[column]) return 1; return 0; } /** * Orders the model by the specified column. * * @param {integer} column the column index **/ ,sort: function (column) { if (!this.checkColExists (column)) return; this.setStatus (Db.Model.Status.LOADING); if (column != this.sortColumn) { this.data.sort (this.sortFunction.bind (this, column)); this.sortColumn = column; } else this.data.reverse (); this.setStatus (Db.Model.Status.READY); } /** * Builds an internal hash index for the specified column, this speeds * significantly searches on that column. * Not implemented yet. * * @param {String} column The column name **/ ,indexColumn: function (column) { this.requestedIndexes[column] = true; if (this._status == Db.Model.Status.READY) this.buildIndex (column); } ,buildIndex: function (column) { var columnIndex = this.getColumnIndex (column); if (columnIndex != -1) { var index = {}; var data = this.data; switch (this.columns[columnIndex].type) { case Db.Conn.Type.TIMESTAMP: case Db.Conn.Type.DATE_TIME: case Db.Conn.Type.DATE: for (var i = 0; i < data.length; i++) index[data[i][columnIndex].toString ()] = i; break; default: for (var i = 0; i < data.length; i++) index[data[i][columnIndex]] = i; } this.indexes[columnIndex] = index; } } /** * Searchs a value on the model and returns the row index of the first * ocurrence. * * @param {String} column The column name * @param {Object} value The value to search * @return {integer} The column index **/ ,search: function (column, value) { var index = this.getColumnIndex (column); return this.searchByIndex (index, value); } /** * Searchs a value on the model and returns the row index of the first * ocurrence. * * @param {integer} col The column index * @param {Object} value The value to search * @return {integer} The column index **/ ,searchByIndex: function (col, value) { if (!this.checkColExists (col)) return -1; if (value) switch (this.columns[col].type) { case Db.Conn.Type.BOOLEAN: value = !!value; break; case Db.Conn.Type.INTEGER: value = parseInt (value); break; case Db.Conn.Type.DOUBLE: value = parseFloat (value); break; default: value = value.toString (); } var index = this.indexes[col]; if (index) { if (index[value]) return index[value]; return -1; } var data = this.data; switch (this.columns[col].type) { case Db.Conn.Type.TIMESTAMP: case Db.Conn.Type.DATE_TIME: case Db.Conn.Type.DATE: { for (var i = 0; i < data.length; i++) if (value === data[i][col].toString ()); return i; break; } default: for (var i = 0; i < data.length; i++) if (value === data[i][col]) return i; } return -1; } // private: ,setStatus: function (status) { this._status = status; this.signalEmit ('status-changed', status); } ,getTarget: function (table) { var x = this.columns; for (var i = 0; i < x.length; i++) if (x[i].table == table) return new Sql.Table ({ name: x[i].orgtable ,schema: x[i].db }); return null; } ,getWhere: function (table, rowIndex, forInsert) { var keyFound = false; var row = this.data[rowIndex]; var andOp = new Sql.Operation ({type: Sql.Operation.Type.AND}); var x = this.columns; for (var i = 0; i < x.length; i++) if (x[i].flags & Db.Conn.Flag.PRI_KEY && x[i].table === table) { var equalOp = new Sql.Operation ({type: Sql.Operation.Type.EQUAL}); equalOp.exprs.add (new Sql.Field ({name: x[i].orgname})); andOp.exprs.add (equalOp); if (row[i]) equalOp.exprs.add (new Sql.Value ({value: row[i]})); else if (x[i].flags & Db.Conn.Flag.AI && forInsert) equalOp.exprs.add (new Sql.Func ({name: 'LAST_INSERT_ID'})); else break; keyFound = true; } return (keyFound) ? andOp : null; } ,sendInsert: function (table, rowIndex, singleColumn, newValue) { var where = this.getWhere (table, rowIndex, true); if (!where) return; var multiStmt = new Sql.MultiStmt (); var target = this.getTarget (table); var insert = new Sql.Insert (); insert.addTarget (target); multiStmt.addStmt (insert); var x = this.defaults; for (var i = 0; i < x.length; i++) if (x[i].table === table) insert.addSet (x[i].field, x[i].value); var x = this.columnDefaults; for (var i = 0; i < x.length; i++) if (x[i].table === table) insert.addSet (x[i].field, this.get (rowIndex, x[i].srcColumn)); var select = new Sql.Select ({where: where}); select.addTarget (target); multiStmt.addStmt (select); var columns = []; var row = this.data[rowIndex]; var x = this.columns; for (var i = 0; i < x.length; i++) if (x[i].table === table) { var setValue = singleColumn != i ? row[i] : newValue; if (setValue) insert.addSet (x[i].orgname, setValue); select.addField (x[i].orgname); columns.push (i); } var updateData = { row: rowIndex ,columns: columns }; this._conn.execStmt (multiStmt, this.updateDone.bind (this, updateData)); } ,selectDone: function (resultSet) { var result; var dataResult; this.cleanData (); for (var i = 0; result = resultSet.fetchResult (); i++) if (i == this._resultIndex) dataResult = result; if (dataResult && typeof dataResult === 'object') { this.sortColumn = -1; this.data = dataResult.data; this.columns = dataResult.columns; this.columnMap = dataResult.columnMap; this.repairColumns (this.columns); this.refreshMainTable (); for (column in this.requestedIndexes) this.buildIndex (column); this.setStatus (Db.Model.Status.READY); } else this.setStatus (Db.Model.Status.ERROR); } ,updateDone: function (updateData, resultSet) { var newValues; if (resultSet.fetchResult () && (newValues = resultSet.fetchRow ())) this.performUpdate (updateData.row, updateData.columns, newValues); } ,performUpdate: function (rowIndex, columns, newValues) { this.signalEmit ('row-updated-before', rowIndex); var row = this.data[rowIndex]; for (var i = 0; i < columns.length; i++) row[columns[i]] = newValues[i]; this.signalEmit ('row-updated', rowIndex, columns); } ,deleteDone: function (rowIndex, resultSet) { if (resultSet.fetchResult()) this.performDelete (rowIndex); } ,performDelete: function (rowIndex) { var row = this.data[rowIndex]; if (!this.requestedMainTable) { this.signalEmit ('row-deleted-before', rowIndex); this.data.splice (rowIndex, 1); this.signalEmit ('row-deleted', rowIndex); } else { this.signalEmit ('row-updated-before', rowIndex); var columns = []; for (var i = 0; i < this.columns.length; i++) if (this.columns[i].table == this._mainTable) { row[i] = null; columns.push (i); } this.signalEmit ('row-updated', rowIndex, columns); } } // Delete when MySQL FLAG and view orgname "bugs" are repaired: ,tableInfo: {} ,fieldFlags: {} ,setTableInfo: function (table, orgtable, db) { this.tableInfo[table] = ({ orgtable: orgtable ,db: db }); } ,setFieldFlags: function (field, flags) { this.fieldFlags[field] = flags; } ,repairColumns: function (columns) { for (var i = 0; i < columns.length; i++) { var newFlags = this.fieldFlags[columns[i].name]; if (newFlags) columns[i].flags |= newFlags; var tableInfo = this.tableInfo[columns[i].table]; if (tableInfo) { columns[i].orgtable = tableInfo.orgtable; columns[i].db = tableInfo.db; } } } });