// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Dtf.WindowsInstaller.Linq { using System; using System.IO; using System.Text; using System.Globalization; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; /// /// Generic record entity for queryable databases, /// and base for strongly-typed entity subclasses. /// /// /// Several predefined specialized subclasses are provided for common /// standard tables. Subclasses for additional standard tables /// or custom tables are not necessary, but they are easy to create /// and make the coding experience much nicer. /// When creating subclasses, the following attributes may be /// useful: , /// /// public class QRecord { /// /// Do not call. Use QTable.NewRecord() instead. /// /// /// Subclasses must also provide a public parameterless constructor. /// QRecord constructors are only public due to implementation /// reasons (to satisfy the new() constraint on the QTable generic /// class). They are not intended to be called by user code other than /// a subclass constructor. If the constructor is invoked directly, /// the record instance will not be properly initialized (associated /// with a database table) and calls to methods on the instance /// will throw a NullReferenceException. /// /// public QRecord() { } internal QDatabase Database { get; set; } internal TableInfo TableInfo { get; set; } internal IList Values { get; set; } internal bool Exists { get; set; } /// /// Gets the number of fields in the record. /// public int FieldCount { get { return this.Values.Count; } } /// /// Gets or sets a record field. /// /// column name of the field /// /// Setting a field value will automatically update the database. /// public string this[string field] { get { if (field == null) { throw new ArgumentNullException("field"); } int index = this.TableInfo.Columns.IndexOf(field); if (index < 0) { throw new ArgumentOutOfRangeException("field"); } return this[index]; } set { if (field == null) { throw new ArgumentNullException("field"); } this.Update(new string[] { field }, new string[] { value }); } } /// /// Gets or sets a record field. /// /// zero-based column index of the field /// /// Setting a field value will automatically update the database. /// public string this[int index] { get { if (index < 0 || index >= this.FieldCount) { throw new ArgumentOutOfRangeException("index"); } return this.Values[index]; } set { if (index < 0 || index >= this.FieldCount) { throw new ArgumentOutOfRangeException("index"); } this.Update(new int[] { index }, new string[] { value }); } } /// /// Used by subclasses to get a field as an integer. /// /// zero-based column index of the field [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "I")] protected int I(int index) { string value = this[index]; return value.Length > 0 ? Int32.Parse(value, CultureInfo.InvariantCulture) : 0; } /// /// Used by subclasses to get a field as a nullable integer. /// /// zero-based column index of the field protected int? NI(int index) { string value = this[index]; return value.Length > 0 ? new int?(Int32.Parse(value, CultureInfo.InvariantCulture)) : null; } /// /// Dumps all record fields to a string. /// public override string ToString() { StringBuilder buf = new StringBuilder(this.GetType().Name); buf.Append(" {"); for (int i = 0; i < this.FieldCount; i++) { buf.AppendFormat("{0} {1} = {2}", (i > 0 ? "," : String.Empty), this.TableInfo.Columns[i].Name, this[i]); } buf.Append(" }"); return buf.ToString(); } /// /// Update multiple fields in the record (and the database). /// /// column names of fields to update /// new values for each field being updated public void Update(IList fields, IList values) { if (fields == null) { throw new ArgumentNullException("fields"); } if (values == null) { throw new ArgumentNullException("values"); } if (fields.Count == 0 || values.Count == 0 || fields.Count > this.FieldCount || values.Count != fields.Count) { throw new ArgumentOutOfRangeException("fields"); } int[] indexes = new int[fields.Count]; for (int i = 0; i < indexes.Length; i++) { if (fields[i] == null) { throw new ArgumentNullException("fields[" + i + "]"); } indexes[i] = this.TableInfo.Columns.IndexOf(fields[i]); if (indexes[i] < 0) { throw new ArgumentOutOfRangeException("fields[" + i + "]"); } } this.Update(indexes, values); } /// /// Update multiple fields in the record (and the database). /// /// column indexes of fields to update /// new values for each field being updated /// /// The record (primary keys) must already exist in the table. /// Updating primary key fields is not yet implemented; use Delete() /// and Insert() instead. /// public void Update(IList indexes, IList values) { if (indexes == null) { throw new ArgumentNullException("indexes"); } if (values == null) { throw new ArgumentNullException("values"); } if (indexes.Count == 0 || values.Count == 0 || indexes.Count > this.FieldCount || values.Count != indexes.Count) { throw new ArgumentOutOfRangeException("indexes"); } bool primaryKeyChanged = false; for (int i = 0; i < indexes.Count; i++) { int index = indexes[i]; if (index < 0 || index >= this.FieldCount) { throw new ArgumentOutOfRangeException("index[" + i + "]"); } ColumnInfo col = this.TableInfo.Columns[index]; if (this.TableInfo.PrimaryKeys.Contains(col.Name)) { if (values[i] == null) { throw new ArgumentNullException("values[" + i + "]"); } primaryKeyChanged = true; } else if (values[i] == null) { if (col.IsRequired) { throw new ArgumentNullException("values[" + i + "]"); } } this.Values[index] = values[i]; } if (this.Exists) { if (!primaryKeyChanged) { int updateRecSize = indexes.Count + this.TableInfo.PrimaryKeys.Count; using (Record updateRec = this.Database.CreateRecord(updateRecSize)) { StringBuilder s = new StringBuilder("UPDATE `"); s.Append(this.TableInfo.Name); s.Append("` SET"); for (int i = 0; i < indexes.Count; i++) { ColumnInfo col = this.TableInfo.Columns[indexes[i]]; if (col.Type == typeof(Stream)) { throw new NotSupportedException( "Cannot update stream columns via QRecord."); } int index = indexes[i]; s.AppendFormat("{0} `{1}` = ?", (i > 0 ? "," : String.Empty), col.Name); if (values[i] != null) { updateRec[i + 1] = values[i]; } } for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) { string key = this.TableInfo.PrimaryKeys[i]; s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key); int index = this.TableInfo.Columns.IndexOf(key); updateRec[indexes.Count + i + 1] = this.Values[index]; } string updateSql = s.ToString(); TextWriter log = this.Database.Log; if (log != null) { log.WriteLine(); log.WriteLine(updateSql); for (int field = 1; field <= updateRecSize; field++) { log.WriteLine(" ? = " + updateRec.GetString(field)); } } this.Database.Execute(updateSql, updateRec); } } else { throw new NotImplementedException( "Update() cannot handle changed primary keys yet."); // TODO: // query using old values // update values // View.Replace } } } /// /// Inserts the record in the database. /// /// /// The record (primary keys) may not already exist in the table. /// Use to get a new /// record. Prmary keys and all required fields /// must be filled in before insertion. /// public void Insert() { this.Insert(false); } /// /// Inserts the record into the table. /// /// true if the record is temporarily /// inserted, to be visible only as long as the database is open /// /// The record (primary keys) may not already exist in the table. /// Use to get a new /// record. Prmary keys and all required fields /// must be filled in before insertion. /// public void Insert(bool temporary) { using (Record updateRec = this.Database.CreateRecord(this.FieldCount)) { string insertSql = this.TableInfo.SqlInsertString; if (temporary) { insertSql += " TEMPORARY"; } TextWriter log = this.Database.Log; if (log != null) { log.WriteLine(); log.WriteLine(insertSql); } for (int index = 0; index < this.FieldCount; index++) { ColumnInfo col = this.TableInfo.Columns[index]; if (col.Type == typeof(Stream)) { throw new NotSupportedException( "Cannot insert stream columns via QRecord."); } if (this.Values[index] != null) { updateRec[index + 1] = this.Values[index]; } if (log != null) { log.WriteLine(" ? = " + this.Values[index]); } } this.Database.Execute(insertSql, updateRec); this.Exists = true; } } /// /// Deletes the record from the table if it exists. /// public void Delete() { using (Record keyRec = this.Database.CreateRecord(this.TableInfo.PrimaryKeys.Count)) { StringBuilder s = new StringBuilder("DELETE FROM `"); s.Append(this.TableInfo.Name); s.Append("`"); for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) { string key = this.TableInfo.PrimaryKeys[i]; s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key); int index = this.TableInfo.Columns.IndexOf(key); keyRec[i + 1] = this.Values[index]; } string deleteSql = s.ToString(); TextWriter log = this.Database.Log; if (log != null) { log.WriteLine(); log.WriteLine(deleteSql); for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) { log.WriteLine(" ? = " + keyRec.GetString(i + 1)); } } this.Database.Execute(deleteSql, keyRec); this.Exists = false; } } /// /// Not yet implemented. /// public void Refresh() { throw new NotImplementedException(); } /// /// Not yet implemented. /// public void Assign() { throw new NotImplementedException(); } /// /// Not yet implemented. /// public bool Merge() { throw new NotImplementedException(); } /// /// Not yet implemented. /// public ICollection Validate() { throw new NotImplementedException(); } /// /// Not yet implemented. /// [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] public ICollection ValidateNew() { throw new NotImplementedException(); } /// /// Not yet implemented. /// public ICollection ValidateFields() { throw new NotImplementedException(); } /// /// Not yet implemented. /// public ICollection ValidateDelete() { throw new NotImplementedException(); } } }