/// Because this constructor initiates database access, it cannot be used with a
/// running installation.
///
/// The Database 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:
/// MsiOpenDatabase
///
public Database(string filePath)
: this(filePath, DatabaseOpenMode.ReadOnly)
{
}
///
/// When a database is opened as the output of another database, the summary information stream
/// of the output database is actually a read-only mirror of the original database and thus cannot
/// be changed. Additionally, it is not persisted with the database. To create or modify the
/// summary information for the output database it must be closed and re-opened.
///
/// The Database 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.
///
/// The database is opened in mode, and will be
/// automatically commited when it is closed.
///
/// Win32 MSI API:
/// MsiOpenDatabase
///
public Database(string filePath, string outputPath)
: this((IntPtr) Database.Open(filePath, outputPath), true, outputPath, DatabaseOpenMode.CreateDirect)
{
}
///
/// Because this constructor initiates database access, it cannot be used with a
/// running installation.
///
/// The database object should be d after use.
/// The finalizer will close the handle if it is still open, however due to the nondeterministic
/// nature of finalization 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.
///
/// A database opened in or
/// mode will be automatically commited when it is
/// closed. However a database opened in or
/// mode must have the method
/// called before it is closed, otherwise no changes will be persisted.
///
/// Win32 MSI API:
/// MsiOpenDatabase
///
public Database(string filePath, DatabaseOpenMode mode)
: this((IntPtr) Database.Open(filePath, mode), true, filePath, mode)
{
}
///
/// Once an item is scheduled, it cannot be unscheduled.
///
/// The items cannot be deleted if the Database object is auto-disposed by the
/// garbage collector; the handle must be explicitly closed.
///
/// Files which are read-only or otherwise locked cannot be deleted,
/// but they will not cause an exception to be thrown.
///
public void DeleteOnClose(string path)
{
if (this.deleteOnClose == null)
{
this.deleteOnClose = new List();
}
this.deleteOnClose.Add(path);
}
///
/// Merges another database with this database.
///
/// The database to be merged into this database
/// Optional name of table to contain the names of the tables containing
/// merge conflicts, the number of conflicting rows within the table, and a reference to the table
/// with the merge conflict.
/// merge failed due to a schema difference or data conflict
/// the Database handle is invalid
///
/// Merge does not copy over embedded cabinet files or embedded transforms from the
/// reference database into the target database. Embedded data streams that are listed in the
/// Binary table or Icon table are copied from the reference database to the target database.
/// Storage embedded in the reference database are not copied to the target database.
///
/// The Merge method merges the data of two databases. These databases must have the same
/// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists
/// if the data in any row in the first database differs from the data in the corresponding row
/// of the second database. Corresponding rows are in the same table of both databases and have
/// the same primary key in both databases. The tables of non-conflicting databases must have
/// the same number of primary keys, same number of columns, same column types, same column names,
/// and the same data in rows with identical primary keys. Temporary columns however don't matter
/// in the column count and corresponding tables can have a different number of temporary columns
/// without creating conflict as long as the persistent columns match.
///
/// If the number, type, or name of columns in corresponding tables are different, the
/// schema of the two databases are incompatible and the installer will stop processing tables
/// and the merge fails. The installer checks that the two databases have the same schema before
/// checking for row merge conflicts. If the schemas are incompatible, the databases have be
/// modified.
///
/// If the data in particular rows differ, this is a row merge conflict, the merge fails
/// and creates a new table with the specified name. The first column of this table is the name
/// of the table having the conflict. The second column gives the number of rows in the table
/// having the conflict.
///
/// Win32 MSI API:
/// MsiDatabaseMerge
///
[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
public void Merge(Database otherDatabase, string errorTable)
{
if (otherDatabase == null)
{
throw new ArgumentNullException("otherDatabase");
}
uint ret = NativeMethods.MsiDatabaseMerge((int) this.Handle, (int) otherDatabase.Handle, errorTable);
if (ret != 0)
{
if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED)
{
throw new MergeException(this, errorTable);
}
else if (ret == (uint) NativeMethods.Error.DATATYPE_MISMATCH)
{
throw new MergeException("Schema difference between the two databases.");
}
else
{
throw InstallerException.ExceptionFromReturnCode(ret);
}
}
}
///
/// Merges another database with this database.
///
/// The database to be merged into this database
/// merge failed due to a schema difference or data conflict
/// the Database handle is invalid
///
/// MsiDatabaseMerge does not copy over embedded cabinet files or embedded transforms from
/// the reference database into the target database. Embedded data streams that are listed in
/// the Binary table or Icon table are copied from the reference database to the target database.
/// Storage embedded in the reference database are not copied to the target database.
///
/// The Merge method merges the data of two databases. These databases must have the same
/// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists
/// if the data in any row in the first database differs from the data in the corresponding row
/// of the second database. Corresponding rows are in the same table of both databases and have
/// the same primary key in both databases. The tables of non-conflicting databases must have
/// the same number of primary keys, same number of columns, same column types, same column names,
/// and the same data in rows with identical primary keys. Temporary columns however don't matter
/// in the column count and corresponding tables can have a different number of temporary columns
/// without creating conflict as long as the persistent columns match.
///
/// If the number, type, or name of columns in corresponding tables are different, the
/// schema of the two databases are incompatible and the installer will stop processing tables
/// and the merge fails. The installer checks that the two databases have the same schema before
/// checking for row merge conflicts. If the schemas are incompatible, the databases have be
/// modified.
///
/// Win32 MSI API:
/// MsiDatabaseMerge
///
public void Merge(Database otherDatabase) { this.Merge(otherDatabase, null); }
///
/// Checks whether a table exists and is persistent in the database.
///
/// The table to the checked
/// true if the table exists and is persistent in the database; false otherwise
/// the table is unknown
/// the Database handle is invalid
///
/// To check whether a table exists regardless of persistence,
/// use .
///
/// Win32 MSI API:
/// MsiDatabaseIsTablePersistent
///
public bool IsTablePersistent(string table)
{
if (String.IsNullOrEmpty(table))
{
throw new ArgumentNullException("table");
}
uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.Handle, table);
if (ret == 3) // MSICONDITION_ERROR
{
throw new InstallerException();
}
return ret == 1;
}
///
/// Checks whether a table contains a persistent column with a given name.
///
/// The table to the checked
/// The name of the column to be checked
/// true if the column exists in the table; false if the column is temporary or does not exist.
/// the View could not be executed
/// the Database handle is invalid
///
/// To check whether a column exists regardless of persistence,
/// use .
///
public bool IsColumnPersistent(string table, string column)
{
if (String.IsNullOrEmpty(table))
{
throw new ArgumentNullException("table");
}
if (String.IsNullOrEmpty(column))
{
throw new ArgumentNullException("column");
}
using (View view = this.OpenView(
"SELECT `Number` FROM `_Columns` WHERE `Table` = '{0}' AND `Name` = '{1}'", table, column))
{
view.Execute();
using (Record rec = view.Fetch())
{
return (rec != null);
}
}
}
///
/// Gets the count of all rows in the table.
///
/// Name of the table whose rows are to be counted
/// The count of all rows in the table
/// the View could not be executed
/// the Database handle is invalid
public int CountRows(string table)
{
return this.CountRows(table, null);
}
///
/// Gets the count of all rows in the table that satisfy a given condition.
///
/// Name of the table whose rows are to be counted
/// Conditional expression, such as could be placed on the end of a SQL WHERE clause
/// The count of all rows in the table satisfying the condition
/// the SQL WHERE syntax is invalid
/// the View could not be executed
/// the Database handle is invalid
public int CountRows(string table, string where)
{
if (String.IsNullOrEmpty(table))
{
throw new ArgumentNullException("table");
}
// to support temporary tables like _Streams, run the query even if the table isn't persistent
TableInfo tableInfo = this.Tables[table];
string primaryKeys = tableInfo == null ? "*" : String.Concat("`", tableInfo.PrimaryKeys[0], "`");
int count;
try
{
using (View view = this.OpenView(
"SELECT {0} FROM `{1}`{2}",
primaryKeys,
table,
(where != null && where.Length != 0 ? " WHERE " + where : "")))
{
view.Execute();
for (count = 0; ; count++)
{
// Avoid creating unnecessary Record objects by not calling View.Fetch().
int recordHandle;
uint ret = RemotableNativeMethods.MsiViewFetch((int)view.Handle, out recordHandle);
if (ret == (uint)NativeMethods.Error.NO_MORE_ITEMS)
{
break;
}
if (ret != 0)
{
throw InstallerException.ExceptionFromReturnCode(ret);
}
RemotableNativeMethods.MsiCloseHandle(recordHandle);
}
}
}
catch (BadQuerySyntaxException)
{
// table was missing
count = 0;
}
return count;
}
///
/// Finalizes the persistent form of the database. All persistent data is written
/// to the writeable database, and no temporary columns or rows are written.
///
/// the Database handle is invalid
///
/// For a database open in mode, this method has no effect.
///
/// For a database open in or
/// mode, it is not necessary to call this method because the database will be automatically committed
/// when it is closed. However this method may be called at any time to persist the current state of tables
/// loaded into memory.
///
/// For a database open in or
/// mode, no changes will be persisted until this method is called. If the database object is closed without
/// calling this method, the database file remains unmodified.
///
/// Win32 MSI API:
/// MsiDatabaseCommit
///
public void Commit()
{
if (this.summaryInfo != null && !this.summaryInfo.IsClosed)
{
this.summaryInfo.Persist();
this.summaryInfo.Close();
this.summaryInfo = null;
}
uint ret = NativeMethods.MsiDatabaseCommit((int) this.Handle);
if (ret != 0)
{
throw InstallerException.ExceptionFromReturnCode(ret);
}
}
///
/// Copies the structure and data from a specified table to a text archive file.
///
/// Name of the table to be exported
/// Path to the file to be created
/// the file path is invalid
/// the Database handle is invalid
///
/// Win32 MSI API:
/// MsiDatabaseExport
///
public void Export(string table, string exportFilePath)
{
if (table == null)
{
throw new ArgumentNullException("table");
}
FileInfo file = new FileInfo(exportFilePath);
uint ret = NativeMethods.MsiDatabaseExport((int) this.Handle, table, file.DirectoryName, file.Name);
if (ret != 0)
{
if (ret == (uint) NativeMethods.Error.BAD_PATHNAME)
{
throw new FileNotFoundException(null, exportFilePath);
}
else
{
throw InstallerException.ExceptionFromReturnCode(ret);
}
}
}
///
/// Imports a database table from a text archive file, dropping any existing table.
///
/// Path to the file to be imported.
/// The table name is specified within the file.
/// the file path is invalid
/// the Database handle is invalid
///
/// Win32 MSI API:
/// MsiDatabaseImport
///
public void Import(string importFilePath)
{
if (String.IsNullOrEmpty(importFilePath))
{
throw new ArgumentNullException("importFilePath");
}
FileInfo file = new FileInfo(importFilePath);
uint ret = NativeMethods.MsiDatabaseImport((int) this.Handle, file.DirectoryName, file.Name);
if (ret != 0)
{
if (ret == (uint) NativeMethods.Error.BAD_PATHNAME)
{
throw new FileNotFoundException(null, importFilePath);
}
else
{
throw InstallerException.ExceptionFromReturnCode(ret);
}
}
}
///
/// Exports all database tables, streams, and summary information to archive files.
///
/// Path to the directory where archive files will be created
/// the directory path is invalid
/// the Database handle is invalid
///
/// The directory will be created if it does not already exist.
///
/// Win32 MSI API:
/// MsiDatabaseExport
///
public void ExportAll(string directoryPath)
{
if (String.IsNullOrEmpty(directoryPath))
{
throw new ArgumentNullException("directoryPath");
}
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
this.Export("_SummaryInformation", Path.Combine(directoryPath, "_SummaryInformation.idt"));
using (View view = this.OpenView("SELECT `Name` FROM `_Tables`"))
{
view.Execute();
foreach (Record rec in view) using (rec)
{
string table = (string) rec[1];
this.Export(table, Path.Combine(directoryPath, table + ".idt"));
}
}
if (!Directory.Exists(Path.Combine(directoryPath, "_Streams")))
{
Directory.CreateDirectory(Path.Combine(directoryPath, "_Streams"));
}
using (View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`"))
{
view.Execute();
foreach (Record rec in view) using (rec)
{
string stream = (string) rec[1];
if (stream.EndsWith("SummaryInformation", StringComparison.Ordinal)) continue;
int i = stream.IndexOf('.');
if (i >= 0)
{
if (File.Exists(Path.Combine(
directoryPath,
Path.Combine(stream.Substring(0, i), stream.Substring(i + 1) + ".ibd"))))
{
continue;
}
}
rec.GetStream(2, Path.Combine(directoryPath, Path.Combine("_Streams", stream)));
}
}
}
///
/// Imports all database tables, streams, and summary information from archive files.
///
/// Path to the directory from which archive files will be imported
/// the directory path is invalid
/// the Database handle is invalid
///
/// Win32 MSI API:
/// MsiDatabaseImport
///
public void ImportAll(string directoryPath)
{
if (String.IsNullOrEmpty(directoryPath))
{
throw new ArgumentNullException("directoryPath");
}
if (File.Exists(Path.Combine(directoryPath, "_SummaryInformation.idt")))
{
this.Import(Path.Combine(directoryPath, "_SummaryInformation.idt"));
}
string[] idtFiles = Directory.GetFiles(directoryPath, "*.idt");
foreach (string file in idtFiles)
{
if (Path.GetFileName(file) != "_SummaryInformation.idt")
{
this.Import(file);
}
}
if (Directory.Exists(Path.Combine(directoryPath, "_Streams")))
{
View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`");
Record rec = null;
try
{
view.Execute();
string[] streamFiles = Directory.GetFiles(Path.Combine(directoryPath, "_Streams"));
foreach (string file in streamFiles)
{
rec = this.CreateRecord(2);
rec[1] = Path.GetFileName(file);
rec.SetStream(2, file);
view.Insert(rec);
rec.Close();
rec = null;
}
}
finally
{
if (rec != null) rec.Close();
view.Close();
}
}
}
///
/// Creates a new record object with the requested number of fields.
///
/// Required number of fields, which may be 0.
/// The maximum number of fields in a record is limited to 65535.
/// A new record object that can be used with the database.
///
/// This method is equivalent to directly calling the
/// constructor in all cases outside of a custom action context. When in a
/// custom action session, this method allows creation of a record that can
/// work with a database other than the session database.
///
/// 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:
/// MsiCreateRecord
///
public Record CreateRecord(int fieldCount)
{
int hRecord = RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, (int) this.Handle);
return new Record((IntPtr) hRecord, true, (View) null);
}
///
/// Returns the file path of this database, or the handle value if a file path was not specified.
///
public override string ToString()
{
if (this.FilePath != null)
{
return this.FilePath;
}
else
{
return "#" + ((int) this.Handle).ToString(CultureInfo.InvariantCulture);
}
}
///
/// Closes the database handle. After closing a handle, further method calls may throw .
///
/// If true, the method has been called directly or
/// indirectly by a user's code, so managed and unmanaged resources will be
/// disposed. If false, only unmanaged resources will be disposed.
protected override void Dispose(bool disposing)
{
if (!this.IsClosed &&
(this.OpenMode == DatabaseOpenMode.CreateDirect ||
this.OpenMode == DatabaseOpenMode.Direct))
{
// Always commit a direct-opened database before closing.
// This avoids unexpected corruption of the database.
this.Commit();
}
base.Dispose(disposing);
if (disposing)
{
if (this.summaryInfo != null)
{
this.summaryInfo.Close();
this.summaryInfo = null;
}
if (this.deleteOnClose != null)
{
foreach (string path in this.deleteOnClose)
{
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
else
{
if (File.Exists(path)) File.Delete(path);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
this.deleteOnClose = null;
}
}
}
private static int Open(string filePath, string outputPath)
{
if (String.IsNullOrEmpty(filePath))
{
throw new ArgumentNullException("filePath");
}
if (String.IsNullOrEmpty(outputPath))
{
throw new ArgumentNullException("outputPath");
}
int hDb;
uint ret = NativeMethods.MsiOpenDatabase(filePath, outputPath, out hDb);
if (ret != 0)
{
throw InstallerException.ExceptionFromReturnCode(ret);
}
return hDb;
}
private static int Open(string filePath, DatabaseOpenMode mode)
{
if (String.IsNullOrEmpty(filePath))
{
throw new ArgumentNullException("filePath");
}
if (Path.GetExtension(filePath).Equals(".msp", StringComparison.Ordinal))
{
const int DATABASEOPENMODE_PATCH = 32;
int patchMode = (int) mode | DATABASEOPENMODE_PATCH;
mode = (DatabaseOpenMode) patchMode;
}
int hDb;
uint ret = NativeMethods.MsiOpenDatabase(filePath, (IntPtr) mode, out hDb);
if (ret != 0)
{
throw InstallerException.ExceptionFromReturnCode(
ret,
String.Format(CultureInfo.InvariantCulture, "Database=\"{0}\"", filePath));
}
return hDb;
}
///
/// Returns the value of the specified property.
///
/// Name of the property to retrieve.
public string ExecutePropertyQuery(string property)
{
IList values = this.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", property);
return (values.Count > 0 ? values[0] : null);
}
}
}