// 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 { using System; using System.Text; using System.Collections.Generic; using System.Globalization; using System.Diagnostics.CodeAnalysis; /// /// A View represents a result set obtained when processing a query using the /// method of a /// . Before any data can be transferred, /// the query must be executed using the method, passing to /// it all replaceable parameters designated within the SQL query string. /// [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] public class View : InstallerHandle, IEnumerable { private Database database; private string sql; private IList tables; private ColumnCollection columns; internal View(IntPtr handle, string sql, Database database) : base(handle, true) { this.sql = sql; this.database = database; } /// /// Gets the Database on which this View was opened. /// public Database Database { get { return this.database; } } /// /// Gets the SQL query string used to open this View. /// public string QueryString { get { return this.sql; } } /// /// Gets the set of tables that were included in the SQL query for this View. /// public IList Tables { get { if (this.tables == null) { if (this.sql == null) { return null; } // Parse the table names out of the SQL query string by looking // for tokens that can come before or after the list of tables. string parseSql = this.sql.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); string upperSql = parseSql.ToUpper(CultureInfo.InvariantCulture); string[] prefixes = new string[] { " FROM ", " INTO ", " TABLE " }; string[] suffixes = new string[] { " WHERE ", " ORDER ", " SET ", " (", " ADD " }; int index; for (int i = 0; i < prefixes.Length; i++) { if ((index = upperSql.IndexOf(prefixes[i], StringComparison.Ordinal)) > 0) { parseSql = parseSql.Substring(index + prefixes[i].Length); upperSql = upperSql.Substring(index + prefixes[i].Length); } } if (upperSql.StartsWith("UPDATE ", StringComparison.Ordinal)) { parseSql = parseSql.Substring(7); upperSql = upperSql.Substring(7); } for (int i = 0; i < suffixes.Length; i++) { if ((index = upperSql.IndexOf(suffixes[i], StringComparison.Ordinal)) > 0) { parseSql = parseSql.Substring(0, index); upperSql = upperSql.Substring(0, index); } } if (upperSql.EndsWith(" HOLD", StringComparison.Ordinal) || upperSql.EndsWith(" FREE", StringComparison.Ordinal)) { parseSql = parseSql.Substring(0, parseSql.Length - 5); upperSql = upperSql.Substring(0, upperSql.Length - 5); } // At this point we should be left with a comma-separated list of table names, // optionally quoted with grave accent marks (`). string[] tableNames = parseSql.Split(','); IList tableList = new List(tableNames.Length); for (int i = 0; i < tableNames.Length; i++) { string tableName = tableNames[i].Trim(' ', '`'); tableList.Add(new TableInfo(this.database, tableName)); } this.tables = tableList; } return new List(this.tables); } } /// /// Gets the set of columns that were included in the query for this View, /// or null if this view is not a SELECT query. /// /// the View is not in an active state /// the View handle is invalid ///

/// Win32 MSI API: /// MsiViewGetColumnInfo ///

public ColumnCollection Columns { get { if (this.columns == null) { this.columns = new ColumnCollection(this); } return this.columns; } } /// /// Executes a SQL View query and supplies any required parameters. The query uses the /// question mark token to represent parameters as described in SQL Syntax. The values of /// these parameters are passed in as the corresponding fields of a parameter record. /// /// Optional Record that supplies the parameters. This /// Record contains values to replace the parameter tokens in the SQL query. /// the View could not be executed /// the View handle is invalid ///

/// Win32 MSI API: /// MsiViewExecute ///

[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Params"), SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public void Execute(Record executeParams) { uint ret = RemotableNativeMethods.MsiViewExecute( (int) this.Handle, (executeParams != null ? (int) executeParams.Handle : 0)); if (ret == (uint) NativeMethods.Error.BAD_QUERY_SYNTAX) { throw new BadQuerySyntaxException(this.sql); } else if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Executes a SQL View query. /// /// the View could not be executed /// the View handle is invalid ///

/// Win32 MSI API: /// MsiViewExecute ///

public void Execute() { this.Execute(null); } /// /// Fetches the next sequential record from the view, or null if there are no more records. /// /// the View is not in an active state /// the View handle is invalid ///

/// The Record object should be d after use. /// It is best that the handle be closed manually as soon as it is no longer /// needed, as leaving lots of unused handles open can degrade performance. ///

/// Win32 MSI API: /// MsiViewFetch ///

public Record Fetch() { int recordHandle; uint ret = RemotableNativeMethods.MsiViewFetch((int) this.Handle, out recordHandle); if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) { return null; } else if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } Record r = new Record((IntPtr) recordHandle, true, this); r.IsFormatStringInvalid = true; return r; } /// /// Updates a fetched Record. /// /// specifies the modify mode /// the Record to modify /// the modification failed, /// or a validation was requested and the data did not pass /// the View handle is invalid ///

/// You can update or delete a record immediately after inserting, or seeking provided you /// have NOT modified the 0th field of the inserted or sought record. ///

/// To execute any SQL statement, a View must be created. However, a View that does not /// create a result set, such as CREATE TABLE, or INSERT INTO, cannot be used with any of /// the Modify methods to update tables though the view. ///

/// You cannot fetch a record containing binary data from one database and then use /// that record to insert the data into another database. To move binary data from one database /// to another, you should export the data to a file and then import it into the new database /// using a query and the . This ensures that each database has /// its own copy of the binary data. ///

/// Note that custom actions can only add, modify, or remove temporary rows, columns, /// or tables from a database. Custom actions cannot modify persistent data in a database, /// such as data that is a part of the database stored on disk. ///

/// Win32 MSI API: /// MsiViewModify ///

/// /// /// /// /// /// /// /// /// /// /// /// /// [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public void Modify(ViewModifyMode mode, Record record) { if (record == null) { throw new ArgumentNullException("record"); } uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) mode, (int) record.Handle); if (mode == ViewModifyMode.Insert || mode == ViewModifyMode.InsertTemporary) { record.IsFormatStringInvalid = true; } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Refreshes the data in a Record. /// /// the Record to be refreshed /// the refresh failed /// the View handle is invalid ///

/// The Record must have been obtained by calling . Fails with /// a deleted Record. Works only with read-write Records. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void Refresh(Record record) { this.Modify(ViewModifyMode.Refresh, record); } /// /// Inserts a Record into the view. /// /// the Record to be inserted /// the insertion failed /// the View handle is invalid ///

/// Fails if a row with the same primary keys exists. Fails with a read-only database. /// This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void Insert(Record record) { this.Modify(ViewModifyMode.Insert, record); } /// /// Updates the View with new data from the Record. /// /// the new data /// the update failed /// the View handle is invalid ///

/// Only non-primary keys can be updated. The Record must have been obtained by calling /// . Fails with a deleted Record. Works only with read-write Records. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void Update(Record record) { this.Modify(ViewModifyMode.Update, record); } /// /// Updates or inserts a Record into the View. /// /// the Record to be assigned /// the assignment failed /// the View handle is invalid ///

/// Updates record if the primary keys match an existing row and inserts if they do not match. /// Fails with a read-only database. This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void Assign(Record record) { this.Modify(ViewModifyMode.Assign, record); } /// /// Updates or deletes and inserts a Record into the View. /// /// the Record to be replaced /// the replacement failed /// the View handle is invalid ///

/// The Record must have been obtained by calling . Updates record if the /// primary keys are unchanged. Deletes old row and inserts new if primary keys have changed. /// Fails with a read-only database. This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void Replace(Record record) { this.Modify(ViewModifyMode.Replace, record); } /// /// Deletes a Record from the View. /// /// the Record to be deleted /// the deletion failed /// the View handle is invalid ///

/// The Record must have been obtained by calling . Fails if the row has been /// deleted. Works only with read-write records. This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void Delete(Record record) { this.Modify(ViewModifyMode.Delete, record); } /// /// Inserts a Record into the View. The inserted data is not persistent. /// /// the Record to be inserted /// the insertion failed /// the View handle is invalid ///

/// Fails if a row with the same primary key exists. Works only with read-write records. /// This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

public void InsertTemporary(Record record) { this.Modify(ViewModifyMode.InsertTemporary, record); } /// /// Refreshes the information in the supplied record without changing the position /// in the result set and without affecting subsequent fetch operations. /// /// the Record to be filled with the result of the seek /// the seek failed /// the View handle is invalid ///

/// After seeking, the Record may then be used for subsequent Update, Delete, and Refresh /// operations. All primary key columns of the table must be in the query and the Record must /// have at least as many fields as the query. Seek cannot be used with multi-table queries. /// This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public bool Seek(Record record) { if (record == null) { throw new ArgumentNullException("record"); } uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) ViewModifyMode.Seek, (int) record.Handle); record.IsFormatStringInvalid = true; if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) { return false; } else if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return true; } /// /// Inserts or validates a record. /// /// the Record to be merged /// true if the record was inserted or validated, false if there is an existing /// record with the same primary keys that is not identical /// the merge failed (for a reason other than invalid data) /// the View handle is invalid ///

/// Works only with read-write records. This method cannot be used with a /// View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI API: /// MsiViewModify ///

[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public bool Merge(Record record) { if (record == null) { throw new ArgumentNullException("record"); } uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) ViewModifyMode.Merge, (int) record.Handle); if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) { return false; } else if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return true; } /// /// Validates a record, returning information about any errors. /// /// the Record to be validated /// null if the record was validated; if there is an existing record with /// the same primary keys that has conflicting data then error information is returned /// the validation failed (for a reason other than invalid data) /// the View handle is invalid ///

/// The Record must have been obtained by calling . /// Works with read-write and read-only records. This method cannot be used /// with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI APIs: /// MsiViewModify, /// MsiViewGetError ///

public ICollection Validate(Record record) { return this.InternalValidate(ViewModifyMode.Validate, record); } /// /// Validates a new record, returning information about any errors. /// /// the Record to be validated /// null if the record was validated; if there is an existing /// record with the same primary keys then error information is returned /// the validation failed (for a reason other than invalid data) /// the View handle is invalid ///

/// Checks for duplicate keys. The Record must have been obtained by /// calling . Works with read-write and read-only records. /// This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI APIs: /// MsiViewModify, /// MsiViewGetError ///

[SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] public ICollection ValidateNew(Record record) { return this.InternalValidate(ViewModifyMode.ValidateNew, record); } /// /// Validates fields of a fetched or new record, returning information about any errors. /// Can validate one or more fields of an incomplete record. /// /// the Record to be validated /// null if the record was validated; if there is an existing record with /// the same primary keys that has conflicting data then error information is returned /// the validation failed (for a reason other than invalid data) /// the View handle is invalid ///

/// Works with read-write and read-only records. This method cannot be used with /// a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI APIs: /// MsiViewModify, /// MsiViewGetError ///

public ICollection ValidateFields(Record record) { return this.InternalValidate(ViewModifyMode.ValidateField, record); } /// /// Validates a record that will be deleted later, returning information about any errors. /// /// the Record to be validated /// null if the record is safe to delete; if another row refers to /// the primary keys of this row then error information is returned /// the validation failed (for a reason other than invalid data) /// the View handle is invalid ///

/// Validation does not check for the existence of the primary keys of this row in properties /// or strings. Does not check if a column is a foreign key to multiple tables. Works with /// read-write and read-only records. This method cannot be used with a View containing joins. ///

/// See for more remarks. ///

/// Win32 MSI APIs: /// MsiViewModify, /// MsiViewGetError ///

public ICollection ValidateDelete(Record record) { return this.InternalValidate(ViewModifyMode.ValidateDelete, record); } /// /// Enumerates over the Records retrieved by the View. /// /// An enumerator of Record objects. /// The View was not d before attempting the enumeration. ///

/// Each Record object should be d after use. /// It is best that the handle be closed manually as soon as it is no longer /// needed, as leaving lots of unused handles open can degrade performance. /// However, note that it is not necessary to complete the enumeration just /// for the purpose of closing handles, because Records are fetched lazily /// on each step of the enumeration. ///

/// Win32 MSI API: /// MsiViewFetch ///

public IEnumerator GetEnumerator() { Record rec; while ((rec = this.Fetch()) != null) { yield return rec; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } private ICollection InternalValidate(ViewModifyMode mode, Record record) { uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) mode, (int) record.Handle); if (ret == (uint) NativeMethods.Error.INVALID_DATA) { ICollection errorInfo = new List(); while (true) { uint bufSize = 40; StringBuilder column = new StringBuilder("", (int) bufSize); int error = RemotableNativeMethods.MsiViewGetError((int) this.Handle, column, ref bufSize); if (error == -2 /*MSIDBERROR_MOREDATA*/) { column.Capacity = (int) ++bufSize; error = RemotableNativeMethods.MsiViewGetError((int) this.Handle, column, ref bufSize); } if (error == -3 /*MSIDBERROR_INVALIDARG*/) { throw InstallerException.ExceptionFromReturnCode((uint) NativeMethods.Error.INVALID_PARAMETER); } else if (error == 0 /*MSIDBERROR_NOERROR*/) { break; } errorInfo.Add(new ValidationErrorInfo((ValidationError) error, column.ToString())); } return errorInfo; } else if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return null; } } }