| 1 | // 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. |
| 2 | |
| 3 | namespace WixToolset.Dtf.WindowsInstaller |
| 4 | { |
| 5 | using System; |
| 6 | using System.IO; |
| 7 | using System.Collections.Generic; |
| 8 | using System.Globalization; |
| 9 | using System.Diagnostics.CodeAnalysis; |
| 10 | |
| 11 | /// <summary> |
| 12 | /// Accesses a Windows Installer database. |
| 13 | /// </summary> |
| 14 | /// <remarks><p> |
| 15 | /// The <see cref="Commit"/> method must be called before the Database is closed to write out all |
| 16 | /// persistent changes. If the Commit method is not called, the installer performs an implicit |
| 17 | /// rollback upon object destruction. |
| 18 | /// </p><p> |
| 19 | /// The client can use the following procedure for data access:<list type="number"> |
| 20 | /// <item><description>Obtain a Database object using one of the Database constructors.</description></item> |
| 21 | /// <item><description>Initiate a query using a SQL string by calling the <see cref="OpenView"/> |
| 22 | /// method of the Database.</description></item> |
| 23 | /// <item><description>Set query parameters in a <see cref="Record"/> and execute the database |
| 24 | /// query by calling the <see cref="View.Execute(Record)"/> method of the <see cref="View"/>. This |
| 25 | /// produces a result that can be fetched or updated.</description></item> |
| 26 | /// <item><description>Call the <see cref="View.Fetch"/> method of the View repeatedly to return |
| 27 | /// Records.</description></item> |
| 28 | /// <item><description>Update database rows of a Record object obtained by the Fetch method using |
| 29 | /// one of the <see cref="View.Modify"/> methods of the View.</description></item> |
| 30 | /// <item><description>Release the query and any unfetched records by calling the <see cref="InstallerHandle.Close"/> |
| 31 | /// method of the View.</description></item> |
| 32 | /// <item><description>Persist any database updates by calling the Commit method of the Database. |
| 33 | /// </description></item> |
| 34 | /// </list> |
| 35 | /// </p></remarks> |
| 36 | public partial class Database : InstallerHandle |
| 37 | { |
| 38 | private string filePath; |
| 39 | private DatabaseOpenMode openMode; |
| 40 | private SummaryInfo summaryInfo; |
| 41 | private TableCollection tables; |
| 42 | private IList<string> deleteOnClose; |
| 43 | |
| 44 | /// <summary> |
| 45 | /// Opens an existing database in read-only mode. |
| 46 | /// </summary> |
| 47 | /// <param name="filePath">Path to the database file.</param> |
| 48 | /// <exception cref="InstallerException">the database could not be created/opened</exception> |
| 49 | /// <remarks><p> |
| 50 | /// Because this constructor initiates database access, it cannot be used with a |
| 51 | /// running installation. |
| 52 | /// </p><p> |
| 53 | /// The Database object should be <see cref="InstallerHandle.Close"/>d after use. |
| 54 | /// It is best that the handle be closed manually as soon as it is no longer |
| 55 | /// needed, as leaving lots of unused handles open can degrade performance. |
| 56 | /// </p><p> |
| 57 | /// Win32 MSI API: |
| 58 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a> |
| 59 | /// </p></remarks> |
| 60 | public Database(string filePath) |
| 61 | : this(filePath, DatabaseOpenMode.ReadOnly) |
| 62 | { |
| 63 | } |
| 64 | |
| 65 | /// <summary> |
| 66 | /// Opens an existing database with another database as output. |
| 67 | /// </summary> |
| 68 | /// <param name="filePath">Path to the database to be read.</param> |
| 69 | /// <param name="outputPath">Open mode for the database</param> |
| 70 | /// <returns>Database object representing the created or opened database</returns> |
| 71 | /// <exception cref="InstallerException">the database could not be created/opened</exception> |
| 72 | /// <remarks><p> |
| 73 | /// When a database is opened as the output of another database, the summary information stream |
| 74 | /// of the output database is actually a read-only mirror of the original database and thus cannot |
| 75 | /// be changed. Additionally, it is not persisted with the database. To create or modify the |
| 76 | /// summary information for the output database it must be closed and re-opened. |
| 77 | /// </p><p> |
| 78 | /// The Database object should be <see cref="InstallerHandle.Close"/>d after use. |
| 79 | /// It is best that the handle be closed manually as soon as it is no longer |
| 80 | /// needed, as leaving lots of unused handles open can degrade performance. |
| 81 | /// </p><p> |
| 82 | /// The database is opened in <see cref="DatabaseOpenMode.CreateDirect" /> mode, and will be |
| 83 | /// automatically commited when it is closed. |
| 84 | /// </p><p> |
| 85 | /// Win32 MSI API: |
| 86 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a> |
| 87 | /// </p></remarks> |
| 88 | public Database(string filePath, string outputPath) |
| 89 | : this((IntPtr) Database.Open(filePath, outputPath), true, outputPath, DatabaseOpenMode.CreateDirect) |
| 90 | { |
| 91 | } |
| 92 | |
| 93 | /// <summary> |
| 94 | /// Opens an existing database or creates a new one. |
| 95 | /// </summary> |
| 96 | /// <param name="filePath">Path to the database file. If an empty string |
| 97 | /// is supplied, a temporary database is created that is not persisted.</param> |
| 98 | /// <param name="mode">Open mode for the database</param> |
| 99 | /// <exception cref="InstallerException">the database could not be created/opened</exception> |
| 100 | /// <remarks><p> |
| 101 | /// Because this constructor initiates database access, it cannot be used with a |
| 102 | /// running installation. |
| 103 | /// </p><p> |
| 104 | /// The database object should be <see cref="InstallerHandle.Close"/>d after use. |
| 105 | /// The finalizer will close the handle if it is still open, however due to the nondeterministic |
| 106 | /// nature of finalization it is best that the handle be closed manually as soon as it is no |
| 107 | /// longer needed, as leaving lots of unused handles open can degrade performance. |
| 108 | /// </p><p> |
| 109 | /// A database opened in <see cref="DatabaseOpenMode.CreateDirect" /> or |
| 110 | /// <see cref="DatabaseOpenMode.Direct" /> mode will be automatically commited when it is |
| 111 | /// closed. However a database opened in <see cref="DatabaseOpenMode.Create" /> or |
| 112 | /// <see cref="DatabaseOpenMode.Transact" /> mode must have the <see cref="Commit" /> method |
| 113 | /// called before it is closed, otherwise no changes will be persisted. |
| 114 | /// </p><p> |
| 115 | /// Win32 MSI API: |
| 116 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a> |
| 117 | /// </p></remarks> |
| 118 | public Database(string filePath, DatabaseOpenMode mode) |
| 119 | : this((IntPtr) Database.Open(filePath, mode), true, filePath, mode) |
| 120 | { |
| 121 | } |
| 122 | |
| 123 | /// <summary> |
| 124 | /// Creates a new database from an MSI handle. |
| 125 | /// </summary> |
| 126 | /// <param name="handle">Native MSI database handle.</param> |
| 127 | /// <param name="ownsHandle">True if the handle should be closed |
| 128 | /// when the database object is disposed</param> |
| 129 | /// <param name="filePath">Path of the database file, if known</param> |
| 130 | /// <param name="openMode">Mode the handle was originally opened in</param> |
| 131 | protected internal Database( |
| 132 | IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode) |
| 133 | : base(handle, ownsHandle) |
| 134 | { |
| 135 | this.filePath = filePath; |
| 136 | this.openMode = openMode; |
| 137 | } |
| 138 | |
| 139 | /// <summary> |
| 140 | /// Gets the file path the Database was originally opened from, or null if not known. |
| 141 | /// </summary> |
| 142 | public String FilePath |
| 143 | { |
| 144 | get |
| 145 | { |
| 146 | return this.filePath; |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /// <summary> |
| 151 | /// Gets the open mode for the database. |
| 152 | /// </summary> |
| 153 | public DatabaseOpenMode OpenMode |
| 154 | { |
| 155 | get |
| 156 | { |
| 157 | return this.openMode; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /// <summary> |
| 162 | /// Gets a boolean value indicating whether this database was opened in read-only mode. |
| 163 | /// </summary> |
| 164 | /// <remarks><p> |
| 165 | /// Win32 MSI API: |
| 166 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetdatabasestate.asp">MsiGetDatabaseState</a> |
| 167 | /// </p></remarks> |
| 168 | public bool IsReadOnly |
| 169 | { |
| 170 | get |
| 171 | { |
| 172 | if (RemotableNativeMethods.RemotingEnabled) |
| 173 | { |
| 174 | return true; |
| 175 | } |
| 176 | |
| 177 | int state = NativeMethods.MsiGetDatabaseState((int) this.Handle); |
| 178 | return state != 1; |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | /// <summary> |
| 183 | /// Gets the collection of tables in the Database. |
| 184 | /// </summary> |
| 185 | public TableCollection Tables |
| 186 | { |
| 187 | get |
| 188 | { |
| 189 | if (this.tables == null) |
| 190 | { |
| 191 | this.tables = new TableCollection(this); |
| 192 | } |
| 193 | return this.tables; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | /// <summary> |
| 198 | /// Gets or sets the code page of the Database. |
| 199 | /// </summary> |
| 200 | /// <exception cref="IOException">error exporting/importing the codepage data</exception> |
| 201 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 202 | /// <remarks><p> |
| 203 | /// Getting or setting the code page is a slow operation because it involves an export or import |
| 204 | /// of the codepage data to/from a temporary file. |
| 205 | /// </p></remarks> |
| 206 | public int CodePage |
| 207 | { |
| 208 | get |
| 209 | { |
| 210 | string tempFile = Path.GetTempFileName(); |
| 211 | StreamReader reader = null; |
| 212 | try |
| 213 | { |
| 214 | this.Export("_ForceCodepage", tempFile); |
| 215 | reader = File.OpenText(tempFile); |
| 216 | reader.ReadLine(); // Skip column name record. |
| 217 | reader.ReadLine(); // Skip column defn record. |
| 218 | string codePageLine = reader.ReadLine(); |
| 219 | return Int32.Parse(codePageLine.Split('\t')[0], CultureInfo.InvariantCulture.NumberFormat); |
| 220 | } |
| 221 | finally |
| 222 | { |
| 223 | if (reader != null) reader.Close(); |
| 224 | File.Delete(tempFile); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | set |
| 229 | { |
| 230 | string tempFile = Path.GetTempFileName(); |
| 231 | StreamWriter writer = null; |
| 232 | try |
| 233 | { |
| 234 | writer = File.AppendText(tempFile); |
| 235 | writer.WriteLine(""); |
| 236 | writer.WriteLine(""); |
| 237 | writer.WriteLine("{0}\t_ForceCodepage", value); |
| 238 | writer.Close(); |
| 239 | writer = null; |
| 240 | this.Import(tempFile); |
| 241 | } |
| 242 | finally |
| 243 | { |
| 244 | if (writer != null) writer.Close(); |
| 245 | File.Delete(tempFile); |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | /// <summary> |
| 251 | /// Gets the SummaryInfo object for this database that can be used to examine and modify properties |
| 252 | /// to the summary information stream. |
| 253 | /// </summary> |
| 254 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 255 | /// <remarks><p> |
| 256 | /// The object returned from this property does not need to be explicitly persisted or closed. |
| 257 | /// Any modifications will be automatically saved when the database is committed. |
| 258 | /// </p><p> |
| 259 | /// Win32 MSI API: |
| 260 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetsummaryinformation.asp">MsiGetSummaryInformation</a> |
| 261 | /// </p></remarks> |
| 262 | public SummaryInfo SummaryInfo |
| 263 | { |
| 264 | get |
| 265 | { |
| 266 | if (this.summaryInfo == null || this.summaryInfo.IsClosed) |
| 267 | { |
| 268 | lock (this.Sync) |
| 269 | { |
| 270 | if (this.summaryInfo == null || this.summaryInfo.IsClosed) |
| 271 | { |
| 272 | int summaryInfoHandle; |
| 273 | int maxProperties = this.IsReadOnly ? 0 : SummaryInfo.MAX_PROPERTIES; |
| 274 | uint ret = RemotableNativeMethods.MsiGetSummaryInformation((int) this.Handle, null, (uint) maxProperties, out summaryInfoHandle); |
| 275 | if (ret != 0) |
| 276 | { |
| 277 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 278 | } |
| 279 | this.summaryInfo = new SummaryInfo((IntPtr) summaryInfoHandle, true); |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | return this.summaryInfo; |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | /// <summary> |
| 288 | /// Creates a new Database object from an integer database handle. |
| 289 | /// </summary> |
| 290 | /// <remarks><p> |
| 291 | /// This method is only provided for interop purposes. A Database object |
| 292 | /// should normally be obtained from <see cref="Session.Database"/> or |
| 293 | /// a public Database constructor. |
| 294 | /// </p></remarks> |
| 295 | /// <param name="handle">Integer database handle</param> |
| 296 | /// <param name="ownsHandle">true to close the handle when this object is disposed</param> |
| 297 | public static Database FromHandle(IntPtr handle, bool ownsHandle) |
| 298 | { |
| 299 | return new Database( |
| 300 | handle, |
| 301 | ownsHandle, |
| 302 | null, |
| 303 | NativeMethods.MsiGetDatabaseState((int) handle) == 1 ? DatabaseOpenMode.Direct : DatabaseOpenMode.ReadOnly); |
| 304 | } |
| 305 | |
| 306 | /// <summary> |
| 307 | /// Schedules a file or directory for deletion after the database handle is closed. |
| 308 | /// </summary> |
| 309 | /// <param name="path">File or directory path to be deleted. All files and subdirectories |
| 310 | /// under a directory are deleted.</param> |
| 311 | /// <remarks><p> |
| 312 | /// Once an item is scheduled, it cannot be unscheduled. |
| 313 | /// </p><p> |
| 314 | /// The items cannot be deleted if the Database object is auto-disposed by the |
| 315 | /// garbage collector; the handle must be explicitly closed. |
| 316 | /// </p><p> |
| 317 | /// Files which are read-only or otherwise locked cannot be deleted, |
| 318 | /// but they will not cause an exception to be thrown. |
| 319 | /// </p></remarks> |
| 320 | public void DeleteOnClose(string path) |
| 321 | { |
| 322 | if (this.deleteOnClose == null) |
| 323 | { |
| 324 | this.deleteOnClose = new List<string>(); |
| 325 | } |
| 326 | this.deleteOnClose.Add(path); |
| 327 | } |
| 328 | |
| 329 | /// <summary> |
| 330 | /// Merges another database with this database. |
| 331 | /// </summary> |
| 332 | /// <param name="otherDatabase">The database to be merged into this database</param> |
| 333 | /// <param name="errorTable">Optional name of table to contain the names of the tables containing |
| 334 | /// merge conflicts, the number of conflicting rows within the table, and a reference to the table |
| 335 | /// with the merge conflict.</param> |
| 336 | /// <exception cref="MergeException">merge failed due to a schema difference or data conflict</exception> |
| 337 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 338 | /// <remarks><p> |
| 339 | /// Merge does not copy over embedded cabinet files or embedded transforms from the |
| 340 | /// reference database into the target database. Embedded data streams that are listed in the |
| 341 | /// Binary table or Icon table are copied from the reference database to the target database. |
| 342 | /// Storage embedded in the reference database are not copied to the target database. |
| 343 | /// </p><p> |
| 344 | /// The Merge method merges the data of two databases. These databases must have the same |
| 345 | /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists |
| 346 | /// if the data in any row in the first database differs from the data in the corresponding row |
| 347 | /// of the second database. Corresponding rows are in the same table of both databases and have |
| 348 | /// the same primary key in both databases. The tables of non-conflicting databases must have |
| 349 | /// the same number of primary keys, same number of columns, same column types, same column names, |
| 350 | /// and the same data in rows with identical primary keys. Temporary columns however don't matter |
| 351 | /// in the column count and corresponding tables can have a different number of temporary columns |
| 352 | /// without creating conflict as long as the persistent columns match. |
| 353 | /// </p><p> |
| 354 | /// If the number, type, or name of columns in corresponding tables are different, the |
| 355 | /// schema of the two databases are incompatible and the installer will stop processing tables |
| 356 | /// and the merge fails. The installer checks that the two databases have the same schema before |
| 357 | /// checking for row merge conflicts. If the schemas are incompatible, the databases have be |
| 358 | /// modified. |
| 359 | /// </p><p> |
| 360 | /// If the data in particular rows differ, this is a row merge conflict, the merge fails |
| 361 | /// and creates a new table with the specified name. The first column of this table is the name |
| 362 | /// of the table having the conflict. The second column gives the number of rows in the table |
| 363 | /// having the conflict. |
| 364 | /// </p><p> |
| 365 | /// Win32 MSI API: |
| 366 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasemerge.asp">MsiDatabaseMerge</a> |
| 367 | /// </p></remarks> |
| 368 | [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] |
| 369 | public void Merge(Database otherDatabase, string errorTable) |
| 370 | { |
| 371 | if (otherDatabase == null) |
| 372 | { |
| 373 | throw new ArgumentNullException("otherDatabase"); |
| 374 | } |
| 375 | |
| 376 | uint ret = NativeMethods.MsiDatabaseMerge((int) this.Handle, (int) otherDatabase.Handle, errorTable); |
| 377 | if (ret != 0) |
| 378 | { |
| 379 | if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) |
| 380 | { |
| 381 | throw new MergeException(this, errorTable); |
| 382 | } |
| 383 | else if (ret == (uint) NativeMethods.Error.DATATYPE_MISMATCH) |
| 384 | { |
| 385 | throw new MergeException("Schema difference between the two databases."); |
| 386 | } |
| 387 | else |
| 388 | { |
| 389 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 390 | } |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | /// <summary> |
| 395 | /// Merges another database with this database. |
| 396 | /// </summary> |
| 397 | /// <param name="otherDatabase">The database to be merged into this database</param> |
| 398 | /// <exception cref="MergeException">merge failed due to a schema difference or data conflict</exception> |
| 399 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 400 | /// <remarks><p> |
| 401 | /// MsiDatabaseMerge does not copy over embedded cabinet files or embedded transforms from |
| 402 | /// the reference database into the target database. Embedded data streams that are listed in |
| 403 | /// the Binary table or Icon table are copied from the reference database to the target database. |
| 404 | /// Storage embedded in the reference database are not copied to the target database. |
| 405 | /// </p><p> |
| 406 | /// The Merge method merges the data of two databases. These databases must have the same |
| 407 | /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists |
| 408 | /// if the data in any row in the first database differs from the data in the corresponding row |
| 409 | /// of the second database. Corresponding rows are in the same table of both databases and have |
| 410 | /// the same primary key in both databases. The tables of non-conflicting databases must have |
| 411 | /// the same number of primary keys, same number of columns, same column types, same column names, |
| 412 | /// and the same data in rows with identical primary keys. Temporary columns however don't matter |
| 413 | /// in the column count and corresponding tables can have a different number of temporary columns |
| 414 | /// without creating conflict as long as the persistent columns match. |
| 415 | /// </p><p> |
| 416 | /// If the number, type, or name of columns in corresponding tables are different, the |
| 417 | /// schema of the two databases are incompatible and the installer will stop processing tables |
| 418 | /// and the merge fails. The installer checks that the two databases have the same schema before |
| 419 | /// checking for row merge conflicts. If the schemas are incompatible, the databases have be |
| 420 | /// modified. |
| 421 | /// </p><p> |
| 422 | /// Win32 MSI API: |
| 423 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasemerge.asp">MsiDatabaseMerge</a> |
| 424 | /// </p></remarks> |
| 425 | public void Merge(Database otherDatabase) { this.Merge(otherDatabase, null); } |
| 426 | |
| 427 | /// <summary> |
| 428 | /// Checks whether a table exists and is persistent in the database. |
| 429 | /// </summary> |
| 430 | /// <param name="table">The table to the checked</param> |
| 431 | /// <returns>true if the table exists and is persistent in the database; false otherwise</returns> |
| 432 | /// <exception cref="ArgumentException">the table is unknown</exception> |
| 433 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 434 | /// <remarks><p> |
| 435 | /// To check whether a table exists regardless of persistence, |
| 436 | /// use <see cref="TableCollection.Contains"/>. |
| 437 | /// </p><p> |
| 438 | /// Win32 MSI API: |
| 439 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseistablepersistent.asp">MsiDatabaseIsTablePersistent</a> |
| 440 | /// </p></remarks> |
| 441 | public bool IsTablePersistent(string table) |
| 442 | { |
| 443 | if (String.IsNullOrEmpty(table)) |
| 444 | { |
| 445 | throw new ArgumentNullException("table"); |
| 446 | } |
| 447 | uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.Handle, table); |
| 448 | if (ret == 3) // MSICONDITION_ERROR |
| 449 | { |
| 450 | throw new InstallerException(); |
| 451 | } |
| 452 | return ret == 1; |
| 453 | } |
| 454 | |
| 455 | /// <summary> |
| 456 | /// Checks whether a table contains a persistent column with a given name. |
| 457 | /// </summary> |
| 458 | /// <param name="table">The table to the checked</param> |
| 459 | /// <param name="column">The name of the column to be checked</param> |
| 460 | /// <returns>true if the column exists in the table; false if the column is temporary or does not exist.</returns> |
| 461 | /// <exception cref="InstallerException">the View could not be executed</exception> |
| 462 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 463 | /// <remarks><p> |
| 464 | /// To check whether a column exists regardless of persistence, |
| 465 | /// use <see cref="ColumnCollection.Contains"/>. |
| 466 | /// </p></remarks> |
| 467 | public bool IsColumnPersistent(string table, string column) |
| 468 | { |
| 469 | if (String.IsNullOrEmpty(table)) |
| 470 | { |
| 471 | throw new ArgumentNullException("table"); |
| 472 | } |
| 473 | if (String.IsNullOrEmpty(column)) |
| 474 | { |
| 475 | throw new ArgumentNullException("column"); |
| 476 | } |
| 477 | using (View view = this.OpenView( |
| 478 | "SELECT `Number` FROM `_Columns` WHERE `Table` = '{0}' AND `Name` = '{1}'", table, column)) |
| 479 | { |
| 480 | view.Execute(); |
| 481 | using (Record rec = view.Fetch()) |
| 482 | { |
| 483 | return (rec != null); |
| 484 | } |
| 485 | } |
| 486 | } |
| 487 | |
| 488 | /// <summary> |
| 489 | /// Gets the count of all rows in the table. |
| 490 | /// </summary> |
| 491 | /// <param name="table">Name of the table whose rows are to be counted</param> |
| 492 | /// <returns>The count of all rows in the table</returns> |
| 493 | /// <exception cref="InstallerException">the View could not be executed</exception> |
| 494 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 495 | public int CountRows(string table) |
| 496 | { |
| 497 | return this.CountRows(table, null); |
| 498 | } |
| 499 | |
| 500 | /// <summary> |
| 501 | /// Gets the count of all rows in the table that satisfy a given condition. |
| 502 | /// </summary> |
| 503 | /// <param name="table">Name of the table whose rows are to be counted</param> |
| 504 | /// <param name="where">Conditional expression, such as could be placed on the end of a SQL WHERE clause</param> |
| 505 | /// <returns>The count of all rows in the table satisfying the condition</returns> |
| 506 | /// <exception cref="BadQuerySyntaxException">the SQL WHERE syntax is invalid</exception> |
| 507 | /// <exception cref="InstallerException">the View could not be executed</exception> |
| 508 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 509 | public int CountRows(string table, string where) |
| 510 | { |
| 511 | if (String.IsNullOrEmpty(table)) |
| 512 | { |
| 513 | throw new ArgumentNullException("table"); |
| 514 | } |
| 515 | |
| 516 | // to support temporary tables like _Streams, run the query even if the table isn't persistent |
| 517 | TableInfo tableInfo = this.Tables[table]; |
| 518 | string primaryKeys = tableInfo == null ? "*" : String.Concat("`", tableInfo.PrimaryKeys[0], "`"); |
| 519 | int count; |
| 520 | |
| 521 | try |
| 522 | { |
| 523 | using (View view = this.OpenView( |
| 524 | "SELECT {0} FROM `{1}`{2}", |
| 525 | primaryKeys, |
| 526 | table, |
| 527 | (where != null && where.Length != 0 ? " WHERE " + where : ""))) |
| 528 | { |
| 529 | view.Execute(); |
| 530 | for (count = 0; ; count++) |
| 531 | { |
| 532 | // Avoid creating unnecessary Record objects by not calling View.Fetch(). |
| 533 | int recordHandle; |
| 534 | uint ret = RemotableNativeMethods.MsiViewFetch((int)view.Handle, out recordHandle); |
| 535 | if (ret == (uint)NativeMethods.Error.NO_MORE_ITEMS) |
| 536 | { |
| 537 | break; |
| 538 | } |
| 539 | |
| 540 | if (ret != 0) |
| 541 | { |
| 542 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 543 | } |
| 544 | |
| 545 | RemotableNativeMethods.MsiCloseHandle(recordHandle); |
| 546 | } |
| 547 | } |
| 548 | } |
| 549 | catch (BadQuerySyntaxException) |
| 550 | { |
| 551 | // table was missing |
| 552 | count = 0; |
| 553 | } |
| 554 | |
| 555 | return count; |
| 556 | } |
| 557 | |
| 558 | /// <summary> |
| 559 | /// Finalizes the persistent form of the database. All persistent data is written |
| 560 | /// to the writeable database, and no temporary columns or rows are written. |
| 561 | /// </summary> |
| 562 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 563 | /// <remarks><p> |
| 564 | /// For a database open in <see cref="DatabaseOpenMode.ReadOnly"/> mode, this method has no effect. |
| 565 | /// </p><p> |
| 566 | /// For a database open in <see cref="DatabaseOpenMode.CreateDirect" /> or <see cref="DatabaseOpenMode.Direct" /> |
| 567 | /// mode, it is not necessary to call this method because the database will be automatically committed |
| 568 | /// when it is closed. However this method may be called at any time to persist the current state of tables |
| 569 | /// loaded into memory. |
| 570 | /// </p><p> |
| 571 | /// For a database open in <see cref="DatabaseOpenMode.Create" /> or <see cref="DatabaseOpenMode.Transact" /> |
| 572 | /// mode, no changes will be persisted until this method is called. If the database object is closed without |
| 573 | /// calling this method, the database file remains unmodified. |
| 574 | /// </p><p> |
| 575 | /// Win32 MSI API: |
| 576 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasecommit.asp">MsiDatabaseCommit</a> |
| 577 | /// </p></remarks> |
| 578 | public void Commit() |
| 579 | { |
| 580 | if (this.summaryInfo != null && !this.summaryInfo.IsClosed) |
| 581 | { |
| 582 | this.summaryInfo.Persist(); |
| 583 | this.summaryInfo.Close(); |
| 584 | this.summaryInfo = null; |
| 585 | } |
| 586 | uint ret = NativeMethods.MsiDatabaseCommit((int) this.Handle); |
| 587 | if (ret != 0) |
| 588 | { |
| 589 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 590 | } |
| 591 | } |
| 592 | |
| 593 | /// <summary> |
| 594 | /// Copies the structure and data from a specified table to a text archive file. |
| 595 | /// </summary> |
| 596 | /// <param name="table">Name of the table to be exported</param> |
| 597 | /// <param name="exportFilePath">Path to the file to be created</param> |
| 598 | /// <exception cref="FileNotFoundException">the file path is invalid</exception> |
| 599 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 600 | /// <remarks><p> |
| 601 | /// Win32 MSI API: |
| 602 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseexport.asp">MsiDatabaseExport</a> |
| 603 | /// </p></remarks> |
| 604 | public void Export(string table, string exportFilePath) |
| 605 | { |
| 606 | if (table == null) |
| 607 | { |
| 608 | throw new ArgumentNullException("table"); |
| 609 | } |
| 610 | |
| 611 | FileInfo file = new FileInfo(exportFilePath); |
| 612 | uint ret = NativeMethods.MsiDatabaseExport((int) this.Handle, table, file.DirectoryName, file.Name); |
| 613 | if (ret != 0) |
| 614 | { |
| 615 | if (ret == (uint) NativeMethods.Error.BAD_PATHNAME) |
| 616 | { |
| 617 | throw new FileNotFoundException(null, exportFilePath); |
| 618 | } |
| 619 | else |
| 620 | { |
| 621 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 622 | } |
| 623 | } |
| 624 | } |
| 625 | |
| 626 | /// <summary> |
| 627 | /// Imports a database table from a text archive file, dropping any existing table. |
| 628 | /// </summary> |
| 629 | /// <param name="importFilePath">Path to the file to be imported. |
| 630 | /// The table name is specified within the file.</param> |
| 631 | /// <exception cref="FileNotFoundException">the file path is invalid</exception> |
| 632 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 633 | /// <remarks><p> |
| 634 | /// Win32 MSI API: |
| 635 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseimport.asp">MsiDatabaseImport</a> |
| 636 | /// </p></remarks> |
| 637 | public void Import(string importFilePath) |
| 638 | { |
| 639 | if (String.IsNullOrEmpty(importFilePath)) |
| 640 | { |
| 641 | throw new ArgumentNullException("importFilePath"); |
| 642 | } |
| 643 | |
| 644 | FileInfo file = new FileInfo(importFilePath); |
| 645 | uint ret = NativeMethods.MsiDatabaseImport((int) this.Handle, file.DirectoryName, file.Name); |
| 646 | if (ret != 0) |
| 647 | { |
| 648 | if (ret == (uint) NativeMethods.Error.BAD_PATHNAME) |
| 649 | { |
| 650 | throw new FileNotFoundException(null, importFilePath); |
| 651 | } |
| 652 | else |
| 653 | { |
| 654 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 655 | } |
| 656 | } |
| 657 | } |
| 658 | |
| 659 | /// <summary> |
| 660 | /// Exports all database tables, streams, and summary information to archive files. |
| 661 | /// </summary> |
| 662 | /// <param name="directoryPath">Path to the directory where archive files will be created</param> |
| 663 | /// <exception cref="FileNotFoundException">the directory path is invalid</exception> |
| 664 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 665 | /// <remarks><p> |
| 666 | /// The directory will be created if it does not already exist. |
| 667 | /// </p><p> |
| 668 | /// Win32 MSI API: |
| 669 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseexport.asp">MsiDatabaseExport</a> |
| 670 | /// </p></remarks> |
| 671 | public void ExportAll(string directoryPath) |
| 672 | { |
| 673 | if (String.IsNullOrEmpty(directoryPath)) |
| 674 | { |
| 675 | throw new ArgumentNullException("directoryPath"); |
| 676 | } |
| 677 | |
| 678 | if (!Directory.Exists(directoryPath)) |
| 679 | { |
| 680 | Directory.CreateDirectory(directoryPath); |
| 681 | } |
| 682 | |
| 683 | this.Export("_SummaryInformation", Path.Combine(directoryPath, "_SummaryInformation.idt")); |
| 684 | |
| 685 | using (View view = this.OpenView("SELECT `Name` FROM `_Tables`")) |
| 686 | { |
| 687 | view.Execute(); |
| 688 | |
| 689 | foreach (Record rec in view) using (rec) |
| 690 | { |
| 691 | string table = (string) rec[1]; |
| 692 | |
| 693 | this.Export(table, Path.Combine(directoryPath, table + ".idt")); |
| 694 | } |
| 695 | } |
| 696 | |
| 697 | if (!Directory.Exists(Path.Combine(directoryPath, "_Streams"))) |
| 698 | { |
| 699 | Directory.CreateDirectory(Path.Combine(directoryPath, "_Streams")); |
| 700 | } |
| 701 | |
| 702 | using (View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`")) |
| 703 | { |
| 704 | view.Execute(); |
| 705 | |
| 706 | foreach (Record rec in view) using (rec) |
| 707 | { |
| 708 | string stream = (string) rec[1]; |
| 709 | if (stream.EndsWith("SummaryInformation", StringComparison.Ordinal)) continue; |
| 710 | |
| 711 | int i = stream.IndexOf('.'); |
| 712 | if (i >= 0) |
| 713 | { |
| 714 | if (File.Exists(Path.Combine( |
| 715 | directoryPath, |
| 716 | Path.Combine(stream.Substring(0, i), stream.Substring(i + 1) + ".ibd")))) |
| 717 | { |
| 718 | continue; |
| 719 | } |
| 720 | } |
| 721 | rec.GetStream(2, Path.Combine(directoryPath, Path.Combine("_Streams", stream))); |
| 722 | } |
| 723 | } |
| 724 | } |
| 725 | |
| 726 | /// <summary> |
| 727 | /// Imports all database tables, streams, and summary information from archive files. |
| 728 | /// </summary> |
| 729 | /// <param name="directoryPath">Path to the directory from which archive files will be imported</param> |
| 730 | /// <exception cref="FileNotFoundException">the directory path is invalid</exception> |
| 731 | /// <exception cref="InvalidHandleException">the Database handle is invalid</exception> |
| 732 | /// <remarks><p> |
| 733 | /// Win32 MSI API: |
| 734 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseimport.asp">MsiDatabaseImport</a> |
| 735 | /// </p></remarks> |
| 736 | public void ImportAll(string directoryPath) |
| 737 | { |
| 738 | if (String.IsNullOrEmpty(directoryPath)) |
| 739 | { |
| 740 | throw new ArgumentNullException("directoryPath"); |
| 741 | } |
| 742 | |
| 743 | if (File.Exists(Path.Combine(directoryPath, "_SummaryInformation.idt"))) |
| 744 | { |
| 745 | this.Import(Path.Combine(directoryPath, "_SummaryInformation.idt")); |
| 746 | } |
| 747 | |
| 748 | string[] idtFiles = Directory.GetFiles(directoryPath, "*.idt"); |
| 749 | foreach (string file in idtFiles) |
| 750 | { |
| 751 | if (Path.GetFileName(file) != "_SummaryInformation.idt") |
| 752 | { |
| 753 | this.Import(file); |
| 754 | } |
| 755 | } |
| 756 | |
| 757 | if (Directory.Exists(Path.Combine(directoryPath, "_Streams"))) |
| 758 | { |
| 759 | View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`"); |
| 760 | Record rec = null; |
| 761 | try |
| 762 | { |
| 763 | view.Execute(); |
| 764 | string[] streamFiles = Directory.GetFiles(Path.Combine(directoryPath, "_Streams")); |
| 765 | foreach (string file in streamFiles) |
| 766 | { |
| 767 | rec = this.CreateRecord(2); |
| 768 | rec[1] = Path.GetFileName(file); |
| 769 | rec.SetStream(2, file); |
| 770 | view.Insert(rec); |
| 771 | rec.Close(); |
| 772 | rec = null; |
| 773 | } |
| 774 | } |
| 775 | finally |
| 776 | { |
| 777 | if (rec != null) rec.Close(); |
| 778 | view.Close(); |
| 779 | } |
| 780 | } |
| 781 | } |
| 782 | |
| 783 | /// <summary> |
| 784 | /// Creates a new record object with the requested number of fields. |
| 785 | /// </summary> |
| 786 | /// <param name="fieldCount">Required number of fields, which may be 0. |
| 787 | /// The maximum number of fields in a record is limited to 65535.</param> |
| 788 | /// <returns>A new record object that can be used with the database.</returns> |
| 789 | /// <remarks><p> |
| 790 | /// This method is equivalent to directly calling the <see cref="Record" /> |
| 791 | /// constructor in all cases outside of a custom action context. When in a |
| 792 | /// custom action session, this method allows creation of a record that can |
| 793 | /// work with a database other than the session database. |
| 794 | /// </p><p> |
| 795 | /// The Record object should be <see cref="InstallerHandle.Close"/>d after use. |
| 796 | /// It is best that the handle be closed manually as soon as it is no longer |
| 797 | /// needed, as leaving lots of unused handles open can degrade performance. |
| 798 | /// </p><p> |
| 799 | /// Win32 MSI API: |
| 800 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a> |
| 801 | /// </p></remarks> |
| 802 | public Record CreateRecord(int fieldCount) |
| 803 | { |
| 804 | int hRecord = RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, (int) this.Handle); |
| 805 | return new Record((IntPtr) hRecord, true, (View) null); |
| 806 | } |
| 807 | |
| 808 | /// <summary> |
| 809 | /// Returns the file path of this database, or the handle value if a file path was not specified. |
| 810 | /// </summary> |
| 811 | public override string ToString() |
| 812 | { |
| 813 | if (this.FilePath != null) |
| 814 | { |
| 815 | return this.FilePath; |
| 816 | } |
| 817 | else |
| 818 | { |
| 819 | return "#" + ((int) this.Handle).ToString(CultureInfo.InvariantCulture); |
| 820 | } |
| 821 | } |
| 822 | |
| 823 | /// <summary> |
| 824 | /// Closes the database handle. After closing a handle, further method calls may throw <see cref="InvalidHandleException"/>. |
| 825 | /// </summary> |
| 826 | /// <param name="disposing">If true, the method has been called directly or |
| 827 | /// indirectly by a user's code, so managed and unmanaged resources will be |
| 828 | /// disposed. If false, only unmanaged resources will be disposed.</param> |
| 829 | protected override void Dispose(bool disposing) |
| 830 | { |
| 831 | if (!this.IsClosed && |
| 832 | (this.OpenMode == DatabaseOpenMode.CreateDirect || |
| 833 | this.OpenMode == DatabaseOpenMode.Direct)) |
| 834 | { |
| 835 | // Always commit a direct-opened database before closing. |
| 836 | // This avoids unexpected corruption of the database. |
| 837 | this.Commit(); |
| 838 | } |
| 839 | |
| 840 | base.Dispose(disposing); |
| 841 | |
| 842 | if (disposing) |
| 843 | { |
| 844 | if (this.summaryInfo != null) |
| 845 | { |
| 846 | this.summaryInfo.Close(); |
| 847 | this.summaryInfo = null; |
| 848 | } |
| 849 | |
| 850 | if (this.deleteOnClose != null) |
| 851 | { |
| 852 | foreach (string path in this.deleteOnClose) |
| 853 | { |
| 854 | try |
| 855 | { |
| 856 | if (Directory.Exists(path)) |
| 857 | { |
| 858 | Directory.Delete(path, true); |
| 859 | } |
| 860 | else |
| 861 | { |
| 862 | if (File.Exists(path)) File.Delete(path); |
| 863 | } |
| 864 | } |
| 865 | catch (IOException) |
| 866 | { |
| 867 | } |
| 868 | catch (UnauthorizedAccessException) |
| 869 | { |
| 870 | } |
| 871 | } |
| 872 | this.deleteOnClose = null; |
| 873 | } |
| 874 | } |
| 875 | } |
| 876 | |
| 877 | private static int Open(string filePath, string outputPath) |
| 878 | { |
| 879 | if (String.IsNullOrEmpty(filePath)) |
| 880 | { |
| 881 | throw new ArgumentNullException("filePath"); |
| 882 | } |
| 883 | |
| 884 | if (String.IsNullOrEmpty(outputPath)) |
| 885 | { |
| 886 | throw new ArgumentNullException("outputPath"); |
| 887 | } |
| 888 | |
| 889 | int hDb; |
| 890 | uint ret = NativeMethods.MsiOpenDatabase(filePath, outputPath, out hDb); |
| 891 | if (ret != 0) |
| 892 | { |
| 893 | throw InstallerException.ExceptionFromReturnCode(ret); |
| 894 | } |
| 895 | return hDb; |
| 896 | } |
| 897 | |
| 898 | private static int Open(string filePath, DatabaseOpenMode mode) |
| 899 | { |
| 900 | if (String.IsNullOrEmpty(filePath)) |
| 901 | { |
| 902 | throw new ArgumentNullException("filePath"); |
| 903 | } |
| 904 | |
| 905 | if (Path.GetExtension(filePath).Equals(".msp", StringComparison.Ordinal)) |
| 906 | { |
| 907 | const int DATABASEOPENMODE_PATCH = 32; |
| 908 | int patchMode = (int) mode | DATABASEOPENMODE_PATCH; |
| 909 | mode = (DatabaseOpenMode) patchMode; |
| 910 | } |
| 911 | |
| 912 | int hDb; |
| 913 | uint ret = NativeMethods.MsiOpenDatabase(filePath, (IntPtr) mode, out hDb); |
| 914 | if (ret != 0) |
| 915 | { |
| 916 | throw InstallerException.ExceptionFromReturnCode( |
| 917 | ret, |
| 918 | String.Format(CultureInfo.InvariantCulture, "Database=\"{0}\"", filePath)); |
| 919 | } |
| 920 | return hDb; |
| 921 | } |
| 922 | |
| 923 | /// <summary> |
| 924 | /// Returns the value of the specified property. |
| 925 | /// </summary> |
| 926 | /// <param name="property">Name of the property to retrieve.</param> |
| 927 | public string ExecutePropertyQuery(string property) |
| 928 | { |
| 929 | IList<string> values = this.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", property); |
| 930 | return (values.Count > 0 ? values[0] : null); |
| 931 | } |
| 932 | } |
| 933 | } |