diff options
Diffstat (limited to '')
-rw-r--r-- | src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs | 933 |
1 files changed, 933 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs new file mode 100644 index 00000000..09627f4b --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs | |||
@@ -0,0 +1,933 @@ | |||
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 | } | ||