diff options
Diffstat (limited to 'src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs')
| -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 | } | ||
