aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2021-05-11 07:36:37 -0700
committerRob Mensching <rob@firegiant.com>2021-05-11 07:36:37 -0700
commit3f583916719eeef598d10a5d4e14ef14f008243b (patch)
tree3d528e0ddb5c0550954217c97059d2f19cd6152a /src/dtf/WixToolset.Dtf.WindowsInstaller.Linq
parent2e5ab696b8b4666d551b2a0532b95fb7fe6dbe03 (diff)
downloadwix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.gz
wix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.bz2
wix-3f583916719eeef598d10a5d4e14ef14f008243b.zip
Merge Dtf
Diffstat (limited to 'src/dtf/WixToolset.Dtf.WindowsInstaller.Linq')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs6
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs60
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs150
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs214
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs501
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs296
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs992
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj21
8 files changed, 2240 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs
new file mode 100644
index 00000000..94abf1dc
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs
@@ -0,0 +1,6 @@
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
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "WixToolset.Dtf.WindowsInstaller.Linq.QTable`1.System.Linq.IQueryable<TRecord>.CreateQuery(System.Linq.Expressions.Expression):System.Linq.IQueryable`1<TElement>")]
6[assembly: SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "WixToolset.Dtf.WindowsInstaller.Linq.QTable`1.System.Linq.IQueryable<TRecord>.Execute(System.Linq.Expressions.Expression):TResult")]
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs
new file mode 100644
index 00000000..60008bc8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs
@@ -0,0 +1,60 @@
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
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6
7 /// <summary>
8 /// Apply to a subclass of QRecord to indicate the name of
9 /// the table the record type is to be used with.
10 /// </summary>
11 /// <remarks>
12 /// If this attribute is not used on a record type, the default
13 /// table name will be derived from the record type name. (An
14 /// optional underscore suffix is stripped.)
15 /// </remarks>
16 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
17 public class DatabaseTableAttribute : Attribute
18 {
19 /// <summary>
20 /// Creates a new DatabaseTableAttribute for the specified table.
21 /// </summary>
22 /// <param name="table">name of the table associated with the record type</param>
23 public DatabaseTableAttribute(string table)
24 {
25 this.Table = table;
26 }
27
28 /// <summary>
29 /// Gets or sets the table associated with the record type.
30 /// </summary>
31 public string Table { get; set; }
32 }
33
34 /// <summary>
35 /// Apply to a property on a subclass of QRecord to indicate
36 /// the name of the column the property is to be associated with.
37 /// </summary>
38 /// <remarks>
39 /// If this attribute is not used on a property, the default
40 /// column name will be the same as the property name.
41 /// </remarks>
42 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
43 public class DatabaseColumnAttribute : Attribute
44 {
45 /// <summary>
46 /// Creates a new DatabaseColumnAttribute which maps a
47 /// record property to a column.
48 /// </summary>
49 /// <param name="column">name of the column associated with the property</param>
50 public DatabaseColumnAttribute(string column)
51 {
52 this.Column = column;
53 }
54
55 /// <summary>
56 /// Gets or sets the column associated with the record property.
57 /// </summary>
58 public string Column { get; set; }
59 }
60}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs
new file mode 100644
index 00000000..1c51b861
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs
@@ -0,0 +1,150 @@
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
3namespace WixToolset.Dtf.WindowsInstaller.Linq.Entities
4{
5 // Silence warnings about style and doc-comments
6 #if !CODE_ANALYSIS
7 #pragma warning disable 1591
8 #region Generated code
9
10 public class Component_ : QRecord
11 {
12 public string Component { get { return this[0]; } set { this[0] = value; } }
13 public string ComponentId { get { return this[1]; } set { this[1] = value; } }
14 public string Directory_ { get { return this[2]; } set { this[2] = value; } }
15 public string Condition { get { return this[4]; } set { this[4] = value; } }
16 public string KeyPath { get { return this[5]; } set { this[5] = value; } }
17 public ComponentAttributes Attributes
18 { get { return (ComponentAttributes) this.I(3); } set { this[3] = ((int) value).ToString(); } }
19 }
20
21 public class CreateFolder_ : QRecord
22 {
23 public string Directory_ { get { return this[0]; } set { this[0] = value; } }
24 public string Component_ { get { return this[1]; } set { this[1] = value; } }
25 }
26
27 public class CustomAction_ : QRecord
28 {
29 public string Action { get { return this[0]; } set { this[0] = value; } }
30 public string Source { get { return this[2]; } set { this[2] = value; } }
31 public string Target { get { return this[3]; } set { this[3] = value; } }
32 public CustomActionTypes Type
33 { get { return (CustomActionTypes) this.I(1); } set { this[1] = ((int) value).ToString(); } }
34 }
35
36 public class Directory_ : QRecord
37 {
38 public string Directory { get { return this[0]; } set { this[0] = value; } }
39 public string Directory_Parent { get { return this[1]; } set { this[1] = value; } }
40 public string DefaultDir { get { return this[2]; } set { this[2] = value; } }
41 }
42
43 public class DuplicateFile_ : QRecord
44 {
45 public string FileKey { get { return this[0]; } set { this[0] = value; } }
46 public string Component_ { get { return this[1]; } set { this[1] = value; } }
47 public string File_ { get { return this[2]; } set { this[2] = value; } }
48 public string DestName { get { return this[4]; } set { this[4] = value; } }
49 public string DestFolder { get { return this[5]; } set { this[5] = value; } }
50 }
51
52 public class Feature_ : QRecord
53 {
54 public string Feature { get { return this[0]; } set { this[0] = value; } }
55 public string Feature_Parent { get { return this[1]; } set { this[1] = value; } }
56 public string Title { get { return this[2]; } set { this[2] = value; } }
57 public string Description { get { return this[3]; } set { this[3] = value; } }
58 public int? Display { get { return this.NI(4); } set { this[4] = value.ToString(); } }
59 public int Level { get { return this.I(5); } set { this[5] = value.ToString(); } }
60 public string Directory_ { get { return this[6]; } set { this[6] = value; } }
61 public FeatureAttributes Attributes
62 { get { return (FeatureAttributes) this.I(7); } set { this[7] = ((int) value).ToString(); } }
63 }
64
65 [DatabaseTable("FeatureComponents")]
66 public class FeatureComponent_ : QRecord
67 {
68 public string Feature_ { get { return this[0]; } set { this[0] = value; } }
69 public string Component_ { get { return this[1]; } set { this[1] = value; } }
70 }
71
72 public class File_ : QRecord
73 {
74 public string File { get { return this[0]; } set { this[0] = value; } }
75 public string Component_ { get { return this[1]; } set { this[1] = value; } }
76 public string FileName { get { return this[2]; } set { this[2] = value; } }
77 public int FileSize { get { return this.I(3); } set { this[3] = value.ToString(); } }
78 public string Version { get { return this[4]; } set { this[4] = value; } }
79 public string Language { get { return this[5]; } set { this[5] = value; } }
80 public int Sequence { get { return this.I(7); } set { this[7] = value.ToString(); } }
81 public FileAttributes Attributes
82 { get { return (FileAttributes) this.I(6); } set { this[6] = ((int) value).ToString(); } }
83 }
84
85 [DatabaseTable("MsiFileHash")]
86 public class FileHash_ : QRecord
87 {
88 public string File_ { get { return this[0]; } set { this[0] = value; } }
89 public int Options { get { return this.I(1); } set { this[1] = value.ToString(); } }
90 public int HashPart1 { get { return this.I(2); } set { this[2] = value.ToString(); } }
91 public int HashPart2 { get { return this.I(3); } set { this[3] = value.ToString(); } }
92 public int HashPart3 { get { return this.I(4); } set { this[4] = value.ToString(); } }
93 public int HashPart4 { get { return this.I(5); } set { this[5] = value.ToString(); } }
94 }
95
96 [DatabaseTable("InstallExecuteSequence")]
97 public class InstallSequence_ : QRecord
98 {
99 public string Action { get { return this[0]; } set { this[0] = value; } }
100 public string Condition { get { return this[1]; } set { this[1] = value; } }
101 public int Sequence { get { return this.I(2); } set { this[2] = value.ToString(); } }
102 }
103
104 public class LaunchCondition_ : QRecord
105 {
106 public string Condition { get { return this[0]; } set { this[0] = value; } }
107 public string Description { get { return this[1]; } set { this[1] = value; } }
108 }
109
110 public class Media_ : QRecord
111 {
112 public int DiskId { get { return this.I(0); } set { this[0] = value.ToString(); } }
113 public int LastSequence { get { return this.I(1); } set { this[1] = value.ToString(); } }
114 public string DiskPrompt { get { return this[2]; } set { this[2] = value; } }
115 public string Cabinet { get { return this[3]; } set { this[3] = value; } }
116 public string VolumeLabel { get { return this[4]; } set { this[4] = value; } }
117 public string Source { get { return this[5]; } set { this[5] = value; } }
118 }
119
120 public class Property_ : QRecord
121 {
122 public string Property { get { return this[0]; } set { this[0] = value; } }
123 public string Value { get { return this[1]; } set { this[1] = value; } }
124 }
125
126 public class Registry_ : QRecord
127 {
128 public string Registry { get { return this[0]; } set { this[0] = value; } }
129 public string Key { get { return this[2]; } set { this[2] = value; } }
130 public string Name { get { return this[3]; } set { this[3] = value; } }
131 public string Value { get { return this[4]; } set { this[4] = value; } }
132 public string Component_ { get { return this[5]; } set { this[5] = value; } }
133 public RegistryRoot Root
134 { get { return (RegistryRoot) this.I(1); } set { this[0] = ((int) value).ToString(); } }
135 }
136
137 public class RemoveFile_ : QRecord
138 {
139 public string FileKey { get { return this[0]; } set { this[0] = value; } }
140 public string Component_ { get { return this[2]; } set { this[2] = value; } }
141 public string FileName { get { return this[3]; } set { this[3] = value; } }
142 public string DirProperty { get { return this[4]; } set { this[4] = value; } }
143 public RemoveFileModes InstallMode
144 { get { return (RemoveFileModes) this.I(5); } set { this[5] = ((int) value).ToString(); } }
145 }
146
147 #endregion // Generated code
148 #pragma warning restore 1591
149 #endif // !CODE_ANALYSIS
150}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs
new file mode 100644
index 00000000..b4de2f60
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs
@@ -0,0 +1,214 @@
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
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using WixToolset.Dtf.WindowsInstaller.Linq.Entities;
8
9 /// <summary>
10 /// Allows any Database instance to be converted into a queryable database.
11 /// </summary>
12 public static class Queryable
13 {
14 /// <summary>
15 /// Converts any Database instance into a queryable database.
16 /// </summary>
17 /// <param name="db"></param>
18 /// <returns>Queryable database instance that operates on the same
19 /// MSI handle.</returns>
20 /// <remarks>
21 /// This extension method is meant for convenient on-the-fly conversion.
22 /// If the existing database instance already happens to be a QDatabase,
23 /// then it is returned unchanged. Otherwise since the new database
24 /// carries the same MSI handle, only one of the instances needs to be
25 /// closed, not both.
26 /// </remarks>
27 public static QDatabase AsQueryable(this Database db)
28 {
29 QDatabase qdb = db as QDatabase;
30 if (qdb == null && db != null)
31 {
32 qdb = new QDatabase(db.Handle, true, db.FilePath, db.OpenMode);
33 }
34 return qdb;
35 }
36 }
37
38 /// <summary>
39 /// Queryable MSI database - extends the base Database class with
40 /// LINQ query functionality along with predefined entity types
41 /// for common tables.
42 /// </summary>
43 public class QDatabase : Database
44 {
45 /// <summary>
46 /// Opens an existing database in read-only mode.
47 /// </summary>
48 /// <param name="filePath">Path to the database file.</param>
49 /// <exception cref="InstallerException">the database could not be created/opened</exception>
50 /// <remarks>
51 /// Because this constructor initiates database access, it cannot be used with a
52 /// running installation.
53 /// <para>The Database object should be <see cref="InstallerHandle.Close"/>d after use.
54 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
55 /// nature of finalization it is best that the handle be closed manually as soon as it is no
56 /// longer needed, as leaving lots of unused handles open can degrade performance.</para>
57 /// </remarks>
58 public QDatabase(string filePath)
59 : base(filePath)
60 {
61 }
62
63 /// <summary>
64 /// Opens an existing database with another database as output.
65 /// </summary>
66 /// <param name="filePath">Path to the database to be read.</param>
67 /// <param name="outputPath">Open mode for the database</param>
68 /// <returns>Database object representing the created or opened database</returns>
69 /// <exception cref="InstallerException">the database could not be created/opened</exception>
70 /// <remarks>
71 /// When a database is opened as the output of another database, the summary information stream
72 /// of the output database is actually a read-only mirror of the original database and thus cannot
73 /// be changed. Additionally, it is not persisted with the database. To create or modify the
74 /// summary information for the output database it must be closed and re-opened.
75 /// <para>The returned Database object should be <see cref="InstallerHandle.Close"/>d after use.
76 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
77 /// nature of finalization it is best that the handle be closed manually as soon as it is no
78 /// longer needed, as leaving lots of unused handles open can degrade performance.</para>
79 /// </remarks>
80 public QDatabase(string filePath, string outputPath)
81 : base(filePath, outputPath)
82 {
83 }
84
85 /// <summary>
86 /// Opens an existing database or creates a new one.
87 /// </summary>
88 /// <param name="filePath">Path to the database file. If an empty string
89 /// is supplied, a temporary database is created that is not persisted.</param>
90 /// <param name="mode">Open mode for the database</param>
91 /// <exception cref="InstallerException">the database could not be created/opened</exception>
92 /// <remarks>
93 /// To make and save changes to a database first open the database in transaction,
94 /// create or, or direct mode. After making the changes, always call the Commit method
95 /// before closing the database handle. The Commit method flushes all buffers.
96 /// <para>Always call the Commit method on a database that has been opened in direct
97 /// mode before closing the database. Failure to do this may corrupt the database.</para>
98 /// <para>Because this constructor initiates database access, it cannot be used with a
99 /// running installation.</para>
100 /// <para>The Database object should be <see cref="InstallerHandle.Close"/>d after use.
101 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
102 /// nature of finalization it is best that the handle be closed manually as soon as it is no
103 /// longer needed, as leaving lots of unused handles open can degrade performance.</para>
104 /// </remarks>
105 public QDatabase(string filePath, DatabaseOpenMode mode)
106 : base(filePath, mode)
107 {
108 }
109
110 /// <summary>
111 /// Creates a new database from an MSI handle.
112 /// </summary>
113 /// <param name="handle">Native MSI database handle.</param>
114 /// <param name="ownsHandle">True if the handle should be closed
115 /// when the database object is disposed</param>
116 /// <param name="filePath">Path of the database file, if known</param>
117 /// <param name="openMode">Mode the handle was originally opened in</param>
118 protected internal QDatabase(
119 IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode)
120 : base(handle, ownsHandle, filePath, openMode)
121 {
122 }
123
124 /// <summary>
125 /// Gets or sets a log where all MSI SQL queries are written.
126 /// </summary>
127 /// <remarks>
128 /// The log can be useful for debugging, or simply to watch the LINQ magic in action.
129 /// </remarks>
130 public TextWriter Log { get; set; }
131
132 /// <summary>
133 /// Gets a queryable table from the datbaase.
134 /// </summary>
135 /// <param name="table">name of the table</param>
136 public QTable<QRecord> this[string table]
137 {
138 get
139 {
140 return new QTable<QRecord>(this, table);
141 }
142 }
143
144 #if !CODE_ANALYSIS
145 #region Queryable tables
146
147 /// <summary>Queryable standard table with predefined specialized record type.</summary>
148 public QTable<Component_> Components
149 { get { return new QTable<Component_>(this); } }
150
151 /// <summary>Queryable standard table with predefined specialized record type.</summary>
152 public QTable<CreateFolder_> CreateFolders
153 { get { return new QTable<CreateFolder_>(this); } }
154
155 /// <summary>Queryable standard table with predefined specialized record type.</summary>
156 public QTable<CustomAction_> CustomActions
157 { get { return new QTable<CustomAction_>(this); } }
158
159 /// <summary>Queryable standard table with predefined specialized record type.</summary>
160 public QTable<Directory_> Directories
161 { get { return new QTable<Directory_>(this); } }
162
163 /// <summary>Queryable standard table with predefined specialized record type.</summary>
164 public QTable<DuplicateFile_> DuplicateFiles
165 { get { return new QTable<DuplicateFile_>(this); } }
166
167 /// <summary>Queryable standard table with predefined specialized record type.</summary>
168 public QTable<Feature_> Features
169 { get { return new QTable<Feature_>(this); } }
170
171 /// <summary>Queryable standard table with predefined specialized record type.</summary>
172 public QTable<FeatureComponent_> FeatureComponents
173 { get { return new QTable<FeatureComponent_>(this); } }
174
175 /// <summary>Queryable standard table with predefined specialized record type.</summary>
176 public QTable<File_> Files
177 { get { return new QTable<File_>(this); } }
178
179 /// <summary>Queryable standard table with predefined specialized record type.</summary>
180 public QTable<FileHash_> FileHashes
181 { get { return new QTable<FileHash_>(this); } }
182
183 /// <summary>Queryable standard table with predefined specialized record type.</summary>
184 public QTable<InstallSequence_> InstallExecuteSequences
185 { get { return new QTable<InstallSequence_>(this, "InstallExecuteSequence"); } }
186
187 /// <summary>Queryable standard table with predefined specialized record type.</summary>
188 public QTable<InstallSequence_> InstallUISequences
189 { get { return new QTable<InstallSequence_>(this, "InstallUISequence"); } }
190
191 /// <summary>Queryable standard table with predefined specialized record type.</summary>
192 public QTable<LaunchCondition_> LaunchConditions
193 { get { return new QTable<LaunchCondition_>(this); } }
194
195 /// <summary>Queryable standard table with predefined specialized record type.</summary>
196 public QTable<Media_> Medias
197 { get { return new QTable<Media_>(this); } }
198
199 /// <summary>Queryable standard table with predefined specialized record type.</summary>
200 public QTable<Property_> Properties
201 { get { return new QTable<Property_>(this); } }
202
203 /// <summary>Queryable standard table with predefined specialized record type.</summary>
204 public QTable<Registry_> Registries
205 { get { return new QTable<Registry_>(this); } }
206
207 /// <summary>Queryable standard table with predefined specialized record type.</summary>
208 public QTable<RemoveFile_> RemoveFiles
209 { get { return new QTable<RemoveFile_>(this); } }
210
211 #endregion // Queryable tables
212 #endif // !CODE_ANALYSIS
213 }
214}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs
new file mode 100644
index 00000000..4b3145fd
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs
@@ -0,0 +1,501 @@
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
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Globalization;
9 using System.Collections.Generic;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// Generic record entity for queryable databases,
14 /// and base for strongly-typed entity subclasses.
15 /// </summary>
16 /// <remarks>
17 /// Several predefined specialized subclasses are provided for common
18 /// standard tables. Subclasses for additional standard tables
19 /// or custom tables are not necessary, but they are easy to create
20 /// and make the coding experience much nicer.
21 /// <para>When creating subclasses, the following attributes may be
22 /// useful: <see cref="DatabaseTableAttribute"/>,
23 /// <see cref="DatabaseColumnAttribute"/></para>
24 /// </remarks>
25 public class QRecord
26 {
27 /// <summary>
28 /// Do not call. Use QTable.NewRecord() instead.
29 /// </summary>
30 /// <remarks>
31 /// Subclasses must also provide a public parameterless constructor.
32 /// <para>QRecord constructors are only public due to implementation
33 /// reasons (to satisfy the new() constraint on the QTable generic
34 /// class). They are not intended to be called by user code other than
35 /// a subclass constructor. If the constructor is invoked directly,
36 /// the record instance will not be properly initialized (associated
37 /// with a database table) and calls to methods on the instance
38 /// will throw a NullReferenceException.</para>
39 /// </remarks>
40 /// <seealso cref="QTable&lt;TRecord&gt;.NewRecord()"/>
41 public QRecord()
42 {
43 }
44
45 internal QDatabase Database { get; set; }
46
47 internal TableInfo TableInfo { get; set; }
48
49 internal IList<string> Values { get; set; }
50
51 internal bool Exists { get; set; }
52
53 /// <summary>
54 /// Gets the number of fields in the record.
55 /// </summary>
56 public int FieldCount
57 {
58 get
59 {
60 return this.Values.Count;
61 }
62 }
63
64 /// <summary>
65 /// Gets or sets a record field.
66 /// </summary>
67 /// <param name="field">column name of the field</param>
68 /// <remarks>
69 /// Setting a field value will automatically update the database.
70 /// </remarks>
71 public string this[string field]
72 {
73 get
74 {
75 if (field == null)
76 {
77 throw new ArgumentNullException("field");
78 }
79
80 int index = this.TableInfo.Columns.IndexOf(field);
81 if (index < 0)
82 {
83 throw new ArgumentOutOfRangeException("field");
84 }
85
86 return this[index];
87 }
88
89 set
90 {
91 if (field == null)
92 {
93 throw new ArgumentNullException("field");
94 }
95
96 this.Update(new string[] { field }, new string[] { value });
97 }
98 }
99
100 /// <summary>
101 /// Gets or sets a record field.
102 /// </summary>
103 /// <param name="index">zero-based column index of the field</param>
104 /// <remarks>
105 /// Setting a field value will automatically update the database.
106 /// </remarks>
107 public string this[int index]
108 {
109 get
110 {
111 if (index < 0 || index >= this.FieldCount)
112 {
113 throw new ArgumentOutOfRangeException("index");
114 }
115
116 return this.Values[index];
117 }
118
119 set
120 {
121 if (index < 0 || index >= this.FieldCount)
122 {
123 throw new ArgumentOutOfRangeException("index");
124 }
125
126 this.Update(new int[] { index }, new string[] { value });
127 }
128 }
129
130 /// <summary>
131 /// Used by subclasses to get a field as an integer.
132 /// </summary>
133 /// <param name="index">zero-based column index of the field</param>
134 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "I")]
135 protected int I(int index)
136 {
137 string value = this[index];
138 return value.Length > 0 ?
139 Int32.Parse(value, CultureInfo.InvariantCulture) : 0;
140 }
141
142 /// <summary>
143 /// Used by subclasses to get a field as a nullable integer.
144 /// </summary>
145 /// <param name="index">zero-based column index of the field</param>
146 protected int? NI(int index)
147 {
148 string value = this[index];
149 return value.Length > 0 ?
150 new int?(Int32.Parse(value, CultureInfo.InvariantCulture)) : null;
151 }
152
153 /// <summary>
154 /// Dumps all record fields to a string.
155 /// </summary>
156 public override string ToString()
157 {
158 StringBuilder buf = new StringBuilder(this.GetType().Name);
159 buf.Append(" {");
160 for (int i = 0; i < this.FieldCount; i++)
161 {
162 buf.AppendFormat("{0} {1} = {2}",
163 (i > 0 ? "," : String.Empty),
164 this.TableInfo.Columns[i].Name,
165 this[i]);
166 }
167 buf.Append(" }");
168 return buf.ToString();
169 }
170
171 /// <summary>
172 /// Update multiple fields in the record (and the database).
173 /// </summary>
174 /// <param name="fields">column names of fields to update</param>
175 /// <param name="values">new values for each field being updated</param>
176 public void Update(IList<string> fields, IList<string> values)
177 {
178 if (fields == null)
179 {
180 throw new ArgumentNullException("fields");
181 }
182
183 if (values == null)
184 {
185 throw new ArgumentNullException("values");
186 }
187
188 if (fields.Count == 0 || values.Count == 0 ||
189 fields.Count > this.FieldCount ||
190 values.Count != fields.Count)
191 {
192 throw new ArgumentOutOfRangeException("fields");
193 }
194
195 int[] indexes = new int[fields.Count];
196 for (int i = 0; i < indexes.Length; i++)
197 {
198 if (fields[i] == null)
199 {
200 throw new ArgumentNullException("fields[" + i + "]");
201 }
202
203 indexes[i] = this.TableInfo.Columns.IndexOf(fields[i]);
204
205 if (indexes[i] < 0)
206 {
207 throw new ArgumentOutOfRangeException("fields[" + i + "]");
208 }
209 }
210
211 this.Update(indexes, values);
212 }
213
214 /// <summary>
215 /// Update multiple fields in the record (and the database).
216 /// </summary>
217 /// <param name="indexes">column indexes of fields to update</param>
218 /// <param name="values">new values for each field being updated</param>
219 /// <remarks>
220 /// The record (primary keys) must already exist in the table.
221 /// <para>Updating primary key fields is not yet implemented; use Delete()
222 /// and Insert() instead.</para>
223 /// </remarks>
224 public void Update(IList<int> indexes, IList<string> values)
225 {
226 if (indexes == null)
227 {
228 throw new ArgumentNullException("indexes");
229 }
230
231 if (values == null)
232 {
233 throw new ArgumentNullException("values");
234 }
235
236 if (indexes.Count == 0 || values.Count == 0 ||
237 indexes.Count > this.FieldCount ||
238 values.Count != indexes.Count)
239 {
240 throw new ArgumentOutOfRangeException("indexes");
241 }
242
243 bool primaryKeyChanged = false;
244 for (int i = 0; i < indexes.Count; i++)
245 {
246 int index = indexes[i];
247 if (index < 0 || index >= this.FieldCount)
248 {
249 throw new ArgumentOutOfRangeException("index[" + i + "]");
250 }
251
252 ColumnInfo col = this.TableInfo.Columns[index];
253 if (this.TableInfo.PrimaryKeys.Contains(col.Name))
254 {
255 if (values[i] == null)
256 {
257 throw new ArgumentNullException("values[" + i + "]");
258 }
259
260 primaryKeyChanged = true;
261 }
262 else if (values[i] == null)
263 {
264 if (col.IsRequired)
265 {
266 throw new ArgumentNullException("values[" + i + "]");
267 }
268 }
269
270 this.Values[index] = values[i];
271 }
272
273 if (this.Exists)
274 {
275 if (!primaryKeyChanged)
276 {
277 int updateRecSize = indexes.Count + this.TableInfo.PrimaryKeys.Count;
278 using (Record updateRec = this.Database.CreateRecord(updateRecSize))
279 {
280 StringBuilder s = new StringBuilder("UPDATE `");
281 s.Append(this.TableInfo.Name);
282 s.Append("` SET");
283
284 for (int i = 0; i < indexes.Count; i++)
285 {
286 ColumnInfo col = this.TableInfo.Columns[indexes[i]];
287 if (col.Type == typeof(Stream))
288 {
289 throw new NotSupportedException(
290 "Cannot update stream columns via QRecord.");
291 }
292
293 int index = indexes[i];
294 s.AppendFormat("{0} `{1}` = ?",
295 (i > 0 ? "," : String.Empty),
296 col.Name);
297
298 if (values[i] != null)
299 {
300 updateRec[i + 1] = values[i];
301 }
302 }
303
304 for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++)
305 {
306 string key = this.TableInfo.PrimaryKeys[i];
307 s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key);
308 int index = this.TableInfo.Columns.IndexOf(key);
309 updateRec[indexes.Count + i + 1] = this.Values[index];
310
311 }
312
313 string updateSql = s.ToString();
314 TextWriter log = this.Database.Log;
315 if (log != null)
316 {
317 log.WriteLine();
318 log.WriteLine(updateSql);
319 for (int field = 1; field <= updateRecSize; field++)
320 {
321 log.WriteLine(" ? = " + updateRec.GetString(field));
322 }
323 }
324
325 this.Database.Execute(updateSql, updateRec);
326 }
327 }
328 else
329 {
330 throw new NotImplementedException(
331 "Update() cannot handle changed primary keys yet.");
332 // TODO:
333 // query using old values
334 // update values
335 // View.Replace
336 }
337 }
338 }
339
340 /// <summary>
341 /// Inserts the record in the database.
342 /// </summary>
343 /// <remarks>
344 /// The record (primary keys) may not already exist in the table.
345 /// <para>Use <see cref="QTable&lt;TRecord&gt;.NewRecord()"/> to get a new
346 /// record. Prmary keys and all required fields
347 /// must be filled in before insertion.</para>
348 /// </remarks>
349 public void Insert()
350 {
351 this.Insert(false);
352 }
353
354 /// <summary>
355 /// Inserts the record into the table.
356 /// </summary>
357 /// <param name="temporary">true if the record is temporarily
358 /// inserted, to be visible only as long as the database is open</param>
359 /// <remarks>
360 /// The record (primary keys) may not already exist in the table.
361 /// <para>Use <see cref="QTable&lt;TRecord&gt;.NewRecord()"/> to get a new
362 /// record. Prmary keys and all required fields
363 /// must be filled in before insertion.</para>
364 /// </remarks>
365 public void Insert(bool temporary)
366 {
367 using (Record updateRec = this.Database.CreateRecord(this.FieldCount))
368 {
369 string insertSql = this.TableInfo.SqlInsertString;
370 if (temporary)
371 {
372 insertSql += " TEMPORARY";
373 }
374
375 TextWriter log = this.Database.Log;
376 if (log != null)
377 {
378 log.WriteLine();
379 log.WriteLine(insertSql);
380 }
381
382 for (int index = 0; index < this.FieldCount; index++)
383 {
384 ColumnInfo col = this.TableInfo.Columns[index];
385 if (col.Type == typeof(Stream))
386 {
387 throw new NotSupportedException(
388 "Cannot insert stream columns via QRecord.");
389 }
390
391 if (this.Values[index] != null)
392 {
393 updateRec[index + 1] = this.Values[index];
394 }
395
396 if (log != null)
397 {
398 log.WriteLine(" ? = " + this.Values[index]);
399 }
400 }
401
402 this.Database.Execute(insertSql, updateRec);
403 this.Exists = true;
404 }
405 }
406
407 /// <summary>
408 /// Deletes the record from the table if it exists.
409 /// </summary>
410 public void Delete()
411 {
412 using (Record keyRec = this.Database.CreateRecord(this.TableInfo.PrimaryKeys.Count))
413 {
414 StringBuilder s = new StringBuilder("DELETE FROM `");
415 s.Append(this.TableInfo.Name);
416 s.Append("`");
417 for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++)
418 {
419 string key = this.TableInfo.PrimaryKeys[i];
420 s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key);
421 int index = this.TableInfo.Columns.IndexOf(key);
422 keyRec[i + 1] = this.Values[index];
423 }
424
425 string deleteSql = s.ToString();
426
427 TextWriter log = this.Database.Log;
428 if (log != null)
429 {
430 log.WriteLine();
431 log.WriteLine(deleteSql);
432
433 for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++)
434 {
435 log.WriteLine(" ? = " + keyRec.GetString(i + 1));
436 }
437 }
438
439 this.Database.Execute(deleteSql, keyRec);
440 this.Exists = false;
441 }
442 }
443
444 /// <summary>
445 /// Not yet implemented.
446 /// </summary>
447 public void Refresh()
448 {
449 throw new NotImplementedException();
450 }
451
452 /// <summary>
453 /// Not yet implemented.
454 /// </summary>
455 public void Assign()
456 {
457 throw new NotImplementedException();
458 }
459
460 /// <summary>
461 /// Not yet implemented.
462 /// </summary>
463 public bool Merge()
464 {
465 throw new NotImplementedException();
466 }
467
468 /// <summary>
469 /// Not yet implemented.
470 /// </summary>
471 public ICollection<ValidationErrorInfo> Validate()
472 {
473 throw new NotImplementedException();
474 }
475
476 /// <summary>
477 /// Not yet implemented.
478 /// </summary>
479 [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
480 public ICollection<ValidationErrorInfo> ValidateNew()
481 {
482 throw new NotImplementedException();
483 }
484
485 /// <summary>
486 /// Not yet implemented.
487 /// </summary>
488 public ICollection<ValidationErrorInfo> ValidateFields()
489 {
490 throw new NotImplementedException();
491 }
492
493 /// <summary>
494 /// Not yet implemented.
495 /// </summary>
496 public ICollection<ValidationErrorInfo> ValidateDelete()
497 {
498 throw new NotImplementedException();
499 }
500 }
501}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs
new file mode 100644
index 00000000..e0e1c154
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs
@@ -0,0 +1,296 @@
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
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Reflection;
10 using System.Linq;
11 using System.Linq.Expressions;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Represents one table in a LINQ-queryable Database.
16 /// </summary>
17 /// <typeparam name="TRecord">type that represents one record in the table</typeparam>
18 /// <remarks>
19 /// This class is the primary gateway to all LINQ to MSI query functionality.
20 /// <para>The TRecord generic parameter may be the general <see cref="QRecord" />
21 /// class, or a specialized subclass of QRecord.</para>
22 /// </remarks>
23 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
24 public sealed class QTable<TRecord> : IOrderedQueryable<TRecord>, IQueryProvider
25 where TRecord : QRecord, new()
26 {
27 private QDatabase db;
28 private TableInfo tableInfo;
29
30 /// <summary>
31 /// Infers the name of the table this instance will be
32 /// associated with.
33 /// </summary>
34 /// <returns>table name</returns>
35 /// <remarks>
36 /// The table name is retrieved from a DatabaseTableAttribute
37 /// on the record type if it exists; otherwise the name is
38 /// derived from the name of the record type itself.
39 /// (An optional underscore suffix on the record type name is dropped.)
40 /// </remarks>
41 private static string InferTableName()
42 {
43 foreach (DatabaseTableAttribute attr in typeof(TRecord).GetCustomAttributes(
44 typeof(DatabaseTableAttribute), false))
45 {
46 string tableName = attr.Table;
47 if (!String.IsNullOrEmpty(tableName))
48 {
49 return tableName;
50 }
51 }
52
53 string recordTypeName = typeof(TRecord).Name;
54 if (recordTypeName[recordTypeName.Length - 1] == '_')
55 {
56 return recordTypeName.Substring(0, recordTypeName.Length - 1);
57 }
58 else
59 {
60 return recordTypeName;
61 }
62 }
63
64 /// <summary>
65 /// Creates a new QTable, inferring the table name
66 /// from the name of the record type parameter.
67 /// </summary>
68 /// <param name="db">database that contains the table</param>
69 public QTable(QDatabase db)
70 : this(db, InferTableName())
71 {
72 }
73
74 /// <summary>
75 /// Creates a new QTable with an explicit table name.
76 /// </summary>
77 /// <param name="db">database that contains the table</param>
78 /// <param name="table">name of the table</param>
79 public QTable(QDatabase db, string table)
80 {
81 if (db == null)
82 {
83 throw new ArgumentNullException("db");
84 }
85
86 if (String.IsNullOrEmpty(table))
87 {
88 throw new ArgumentNullException("table");
89 }
90
91 this.db = db;
92 this.tableInfo = db.Tables[table];
93 if (this.tableInfo == null)
94 {
95 throw new ArgumentException(
96 "Table does not exist in database: " + table);
97 }
98 }
99
100 /// <summary>
101 /// Gets schema information about the table.
102 /// </summary>
103 public TableInfo TableInfo
104 {
105 get
106 {
107 return this.tableInfo;
108 }
109 }
110
111 /// <summary>
112 /// Gets the database this table is associated with.
113 /// </summary>
114 public QDatabase Database
115 {
116 get
117 {
118 return this.db;
119 }
120 }
121
122 /// <summary>
123 /// Enumerates over all records in the table.
124 /// </summary>
125 /// <returns></returns>
126 public IEnumerator<TRecord> GetEnumerator()
127 {
128 string query = this.tableInfo.SqlSelectString;
129
130 TextWriter log = this.db.Log;
131 if (log != null)
132 {
133 log.WriteLine();
134 log.WriteLine(query);
135 }
136
137 using (View view = db.OpenView(query))
138 {
139 view.Execute();
140
141 ColumnCollection columns = this.tableInfo.Columns;
142 int columnCount = columns.Count;
143 bool[] isBinary = new bool[columnCount];
144
145 for (int i = 0; i < isBinary.Length; i++)
146 {
147 isBinary[i] = columns[i].Type == typeof(System.IO.Stream);
148 }
149
150 foreach (Record rec in view) using (rec)
151 {
152 string[] values = new string[columnCount];
153 for (int i = 0; i < values.Length; i++)
154 {
155 values[i] = isBinary[i] ? "[Binary Data]" : rec.GetString(i + 1);
156 }
157
158 TRecord trec = new TRecord();
159 trec.Database = this.Database;
160 trec.TableInfo = this.TableInfo;
161 trec.Values = values;
162 trec.Exists = true;
163 yield return trec;
164 }
165 }
166 }
167
168 IEnumerator IEnumerable.GetEnumerator()
169 {
170 return ((IEnumerable<TRecord>) this).GetEnumerator();
171 }
172
173 IQueryable<TElement> IQueryProvider.CreateQuery<TElement>(Expression expression)
174 {
175 if (expression == null)
176 {
177 throw new ArgumentNullException("expression");
178 }
179
180 Query<TElement> q = new Query<TElement>(this.Database, expression);
181
182 MethodCallExpression methodCallExpression = (MethodCallExpression) expression;
183 string methodName = methodCallExpression.Method.Name;
184 if (methodName == "Where")
185 {
186 LambdaExpression argumentExpression = (LambdaExpression)
187 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
188 q.BuildQuery(this.TableInfo, argumentExpression);
189 }
190 else if (methodName == "OrderBy")
191 {
192 LambdaExpression argumentExpression = (LambdaExpression)
193 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
194 q.BuildSequence(this.TableInfo, argumentExpression);
195 }
196 else if (methodName == "Select")
197 {
198 LambdaExpression argumentExpression = (LambdaExpression)
199 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
200 q.BuildNullQuery(this.TableInfo, typeof(TRecord), argumentExpression);
201 q.BuildProjection(null, argumentExpression);
202 }
203 else if (methodName == "Join")
204 {
205 ConstantExpression constantExpression = (ConstantExpression)
206 methodCallExpression.Arguments[1];
207 IQueryable inner = (IQueryable) constantExpression.Value;
208 q.PerformJoin(
209 this.TableInfo,
210 typeof(TRecord),
211 inner,
212 GetJoinLambda(methodCallExpression.Arguments[2]),
213 GetJoinLambda(methodCallExpression.Arguments[3]),
214 GetJoinLambda(methodCallExpression.Arguments[4]));
215 }
216 else
217 {
218 throw new NotSupportedException(
219 "Query operation not supported: " + methodName);
220 }
221
222 return q;
223 }
224
225 private static LambdaExpression GetJoinLambda(Expression expresion)
226 {
227 UnaryExpression unaryExpression = (UnaryExpression) expresion;
228 return (LambdaExpression) unaryExpression.Operand;
229 }
230
231 IQueryable IQueryProvider.CreateQuery(Expression expression)
232 {
233 return ((IQueryProvider) this).CreateQuery<TRecord>(expression);
234 }
235
236 TResult IQueryProvider.Execute<TResult>(Expression expression)
237 {
238 throw new NotSupportedException(
239 "Direct method calls not supported -- use AsEnumerable() instead.");
240 }
241
242 object IQueryProvider.Execute(Expression expression)
243 {
244 throw new NotSupportedException(
245 "Direct method calls not supported -- use AsEnumerable() instead.");
246 }
247
248 IQueryProvider IQueryable.Provider
249 {
250 get
251 {
252 return this;
253 }
254 }
255
256 Type IQueryable.ElementType
257 {
258 get
259 {
260 return typeof(TRecord);
261 }
262 }
263
264 Expression IQueryable.Expression
265 {
266 get
267 {
268 return Expression.Constant(this);
269 }
270 }
271
272 /// <summary>
273 /// Creates a new record that can be inserted into this table.
274 /// </summary>
275 /// <returns>a record with all fields initialized to null</returns>
276 /// <remarks>
277 /// Primary keys and required fields must be filled in with
278 /// non-null values before the record can be inserted.
279 /// <para>The record is tied to this table in this database;
280 /// it cannot be inserted into another table or database.</para>
281 /// </remarks>
282 public TRecord NewRecord()
283 {
284 TRecord rec = new TRecord();
285 rec.Database = this.Database;
286 rec.TableInfo = this.TableInfo;
287 IList<string> values = new List<string>(this.TableInfo.Columns.Count);
288 for (int i = 0; i < this.TableInfo.Columns.Count; i++)
289 {
290 values.Add(null);
291 }
292 rec.Values = values;
293 return rec;
294 }
295 }
296}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs
new file mode 100644
index 00000000..ea58757c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs
@@ -0,0 +1,992 @@
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
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Linq;
10 using System.Linq.Expressions;
11 using System.Text;
12 using System.Reflection;
13 using System.Globalization;
14 using System.Diagnostics.CodeAnalysis;
15
16 /// <summary>
17 /// Implements the LINQ to MSI query functionality.
18 /// </summary>
19 /// <typeparam name="T">the result type of the current query --
20 /// either some kind of QRecord, or some projection of record data</typeparam>
21 internal sealed class Query<T> : IOrderedQueryable<T>, IQueryProvider
22 {
23 private QDatabase db;
24 private Expression queryableExpression;
25 private List<TableInfo> tables;
26 private List<Type> recordTypes;
27 private List<string> selectors;
28 private string where;
29 private List<object> whereParameters;
30 private List<TableColumn> orderbyColumns;
31 private List<TableColumn> selectColumns;
32 private List<TableColumn> joinColumns;
33 private List<Delegate> projectionDelegates;
34
35 internal Query(QDatabase db, Expression expression)
36 {
37 if (expression == null)
38 {
39 throw new ArgumentNullException("expression");
40 }
41
42 this.db = db;
43 this.queryableExpression = expression;
44 this.tables = new List<TableInfo>();
45 this.recordTypes = new List<Type>();
46 this.selectors = new List<string>();
47 this.whereParameters = new List<object>();
48 this.orderbyColumns = new List<TableColumn>();
49 this.selectColumns = new List<TableColumn>();
50 this.joinColumns = new List<TableColumn>();
51 this.projectionDelegates = new List<Delegate>();
52 }
53
54 public IEnumerator<T> GetEnumerator()
55 {
56 if (this.selectColumns.Count == 0)
57 {
58 AddAllColumns(this.tables[0], this.selectColumns);
59 }
60
61 string query = this.CompileQuery();
62 return this.InvokeQuery(query);
63 }
64
65 private string CompileQuery()
66 {
67 bool explicitTables = this.tables.Count > 1;
68
69 StringBuilder queryBuilder = new StringBuilder("SELECT");
70
71 for (int i = 0; i < this.selectColumns.Count; i++)
72 {
73 queryBuilder.AppendFormat(
74 CultureInfo.InvariantCulture,
75 (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"),
76 (i > 0 ? "," : String.Empty),
77 this.selectColumns[i].Table.Name,
78 this.selectColumns[i].Column.Name);
79 }
80
81 for (int i = 0; i < this.tables.Count; i++)
82 {
83 queryBuilder.AppendFormat(
84 CultureInfo.InvariantCulture,
85 "{0} `{1}`",
86 (i == 0 ? " FROM" : ","),
87 this.tables[i].Name);
88 }
89
90 bool startedWhere = false;
91 for (int i = 0; i < this.joinColumns.Count - 1; i += 2)
92 {
93 queryBuilder.AppendFormat(
94 CultureInfo.InvariantCulture,
95 "{0} `{1}`.`{2}` = `{3}`.`{4}` ",
96 (i == 0 ? " WHERE" : "AND"),
97 this.joinColumns[i].Table,
98 this.joinColumns[i].Column,
99 this.joinColumns[i + 1].Table,
100 this.joinColumns[i + 1].Column);
101 startedWhere = true;
102 }
103
104 if (this.where != null)
105 {
106 queryBuilder.Append(startedWhere ? "AND " : " WHERE");
107 queryBuilder.Append(this.where);
108 }
109
110 for (int i = 0; i < this.orderbyColumns.Count; i++)
111 {
112 VerifyOrderByColumn(this.orderbyColumns[i]);
113
114 queryBuilder.AppendFormat(
115 CultureInfo.InvariantCulture,
116 (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"),
117 (i == 0 ? " ORDER BY" : ","),
118 this.orderbyColumns[i].Table.Name,
119 this.orderbyColumns[i].Column.Name);
120 }
121
122 return queryBuilder.ToString();
123 }
124
125 private static void VerifyOrderByColumn(TableColumn tableColumn)
126 {
127 if (tableColumn.Column.Type != typeof(int) &&
128 tableColumn.Column.Type != typeof(short))
129 {
130 throw new NotSupportedException(
131 "Cannot orderby column: " + tableColumn.Column.Name +
132 "; orderby is only supported on integer fields");
133 }
134 }
135
136 private IEnumerator<T> InvokeQuery(string query)
137 {
138 TextWriter log = this.db.Log;
139 if (log != null)
140 {
141 log.WriteLine();
142 log.WriteLine(query);
143 }
144
145 using (View queryView = this.db.OpenView(query))
146 {
147 if (this.whereParameters != null && this.whereParameters.Count > 0)
148 {
149 using (Record paramsRec = this.db.CreateRecord(this.whereParameters.Count))
150 {
151 for (int i = 0; i < this.whereParameters.Count; i++)
152 {
153 paramsRec[i + 1] = this.whereParameters[i];
154
155 if (log != null)
156 {
157 log.WriteLine(" ? = " + this.whereParameters[i]);
158 }
159 }
160
161 queryView.Execute(paramsRec);
162 }
163 }
164 else
165 {
166 queryView.Execute();
167 }
168
169 foreach (Record resultRec in queryView) using (resultRec)
170 {
171 yield return this.GetResult(resultRec);
172 }
173 }
174 }
175
176 private T GetResult(Record resultRec)
177 {
178 object[] results = new object[this.tables.Count];
179
180 for (int i = 0; i < this.tables.Count; i++)
181 {
182 string[] values = new string[this.tables[i].Columns.Count];
183 for (int j = 0; j < this.selectColumns.Count; j++)
184 {
185 TableColumn col = this.selectColumns[j];
186 if (col.Table.Name == this.tables[i].Name)
187 {
188 int index = this.tables[i].Columns.IndexOf(
189 col.Column.Name);
190 if (index >= 0)
191 {
192 if (col.Column.Type == typeof(Stream))
193 {
194 values[index] = "[Binary Data]";
195 }
196 else
197 {
198 values[index] = resultRec.GetString(j + 1);
199 }
200 }
201 }
202 }
203
204 QRecord result = (QRecord) this.recordTypes[i]
205 .GetConstructor(Type.EmptyTypes).Invoke(null);
206 result.Database = this.db;
207 result.TableInfo = this.tables[i];
208 result.Values = values;
209 result.Exists = true;
210 results[i] = result;
211 }
212
213 if (this.projectionDelegates.Count > 0)
214 {
215 object resultsProjection = results[0];
216 for (int i = 1; i <= results.Length; i++)
217 {
218 if (i < results.Length)
219 {
220 resultsProjection = this.projectionDelegates[i - 1]
221 .DynamicInvoke(new object[] { resultsProjection, results[i] });
222 }
223 else
224 {
225 resultsProjection = this.projectionDelegates[i - 1]
226 .DynamicInvoke(resultsProjection);
227 }
228 }
229
230 return (T) resultsProjection;
231 }
232 else
233 {
234 return (T) (object) results[0];
235 }
236 }
237
238 IEnumerator IEnumerable.GetEnumerator()
239 {
240 return ((IEnumerable<T>) this).GetEnumerator();
241 }
242
243 public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
244 {
245 if (expression == null)
246 {
247 throw new ArgumentNullException("expression");
248 }
249
250 Query<TElement> q = new Query<TElement>(this.db, expression);
251 q.tables.AddRange(this.tables);
252 q.recordTypes.AddRange(this.recordTypes);
253 q.selectors.AddRange(this.selectors);
254 q.where = this.where;
255 q.whereParameters.AddRange(this.whereParameters);
256 q.orderbyColumns.AddRange(this.orderbyColumns);
257 q.selectColumns.AddRange(this.selectColumns);
258 q.joinColumns.AddRange(this.joinColumns);
259 q.projectionDelegates.AddRange(this.projectionDelegates);
260
261 MethodCallExpression methodCallExpression = (MethodCallExpression) expression;
262 string methodName = methodCallExpression.Method.Name;
263 if (methodName == "Select")
264 {
265 LambdaExpression argumentExpression = (LambdaExpression)
266 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
267 q.BuildProjection(null, argumentExpression);
268 }
269 else if (methodName == "Where")
270 {
271 LambdaExpression argumentExpression = (LambdaExpression)
272 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
273 q.BuildQuery(null, argumentExpression);
274 }
275 else if (methodName == "ThenBy")
276 {
277 LambdaExpression argumentExpression = (LambdaExpression)
278 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
279 q.BuildSequence(null, argumentExpression);
280 }
281 else if (methodName == "Join")
282 {
283 ConstantExpression constantExpression = (ConstantExpression)
284 methodCallExpression.Arguments[1];
285 IQueryable inner = (IQueryable) constantExpression.Value;
286 q.PerformJoin(
287 null,
288 null,
289 inner,
290 GetJoinLambda(methodCallExpression.Arguments[2]),
291 GetJoinLambda(methodCallExpression.Arguments[3]),
292 GetJoinLambda(methodCallExpression.Arguments[4]));
293 }
294 else
295 {
296 throw new NotSupportedException(
297 "Query operation not supported: " + methodName);
298 }
299
300 return q;
301 }
302
303 public IQueryable CreateQuery(Expression expression)
304 {
305 return this.CreateQuery<T>(expression);
306 }
307
308 private static LambdaExpression GetJoinLambda(Expression expresion)
309 {
310 UnaryExpression unaryExpression = (UnaryExpression) expresion;
311 return (LambdaExpression) unaryExpression.Operand;
312 }
313
314 public TResult Execute<TResult>(Expression expression)
315 {
316 throw new NotSupportedException(
317 "Direct method calls not supported -- use AsEnumerable() instead.");
318 }
319
320 object IQueryProvider.Execute(Expression expression)
321 {
322 throw new NotSupportedException(
323 "Direct method calls not supported -- use AsEnumerable() instead.");
324 }
325
326 public IQueryProvider Provider
327 {
328 get
329 {
330 return this;
331 }
332 }
333
334 public Type ElementType
335 {
336 get
337 {
338 return typeof(T);
339 }
340 }
341
342 public Expression Expression
343 {
344 get
345 {
346 return this.queryableExpression;
347 }
348 }
349
350 internal void BuildQuery(TableInfo tableInfo, LambdaExpression expression)
351 {
352 if (tableInfo != null)
353 {
354 this.tables.Add(tableInfo);
355 this.recordTypes.Add(typeof(T));
356 this.selectors.Add(expression.Parameters[0].Name);
357 }
358
359 StringBuilder queryBuilder = new StringBuilder();
360
361 this.ParseQuery(expression.Body, queryBuilder);
362
363 this.where = queryBuilder.ToString();
364 }
365
366 internal void BuildNullQuery(TableInfo tableInfo, Type recordType, LambdaExpression expression)
367 {
368 this.tables.Add(tableInfo);
369 this.recordTypes.Add(recordType);
370 this.selectors.Add(expression.Parameters[0].Name);
371 }
372
373 private void ParseQuery(Expression expression, StringBuilder queryBuilder)
374 {
375 queryBuilder.Append("(");
376
377 BinaryExpression binaryExpression;
378 UnaryExpression unaryExpression;
379 MethodCallExpression methodCallExpression;
380
381 if ((binaryExpression = expression as BinaryExpression) != null)
382 {
383 switch (binaryExpression.NodeType)
384 {
385 case ExpressionType.AndAlso:
386 this.ParseQuery(binaryExpression.Left, queryBuilder);
387 queryBuilder.Append(" AND ");
388 this.ParseQuery(binaryExpression.Right, queryBuilder);
389 break;
390
391 case ExpressionType.OrElse:
392 this.ParseQuery(binaryExpression.Left, queryBuilder);
393 queryBuilder.Append(" OR ");
394 this.ParseQuery(binaryExpression.Right, queryBuilder);
395 break;
396
397 case ExpressionType.Equal:
398 case ExpressionType.NotEqual:
399 case ExpressionType.GreaterThan:
400 case ExpressionType.LessThan:
401 case ExpressionType.GreaterThanOrEqual:
402 case ExpressionType.LessThanOrEqual:
403 this.ParseQueryCondition(binaryExpression, queryBuilder);
404 break;
405
406 default:
407 throw new NotSupportedException(
408 "Expression type not supported: " + binaryExpression.NodeType );
409 }
410 }
411 else if ((unaryExpression = expression as UnaryExpression) != null)
412 {
413 throw new NotSupportedException(
414 "Expression type not supported: " + unaryExpression.NodeType);
415 }
416 else if ((methodCallExpression = expression as MethodCallExpression) != null)
417 {
418 throw new NotSupportedException(
419 "Method call not supported: " + methodCallExpression.Method.Name + "()");
420 }
421 else
422 {
423 throw new NotSupportedException(
424 "Query filter expression not supported: " + expression);
425 }
426
427 queryBuilder.Append(")");
428 }
429
430 private static ExpressionType OppositeExpression(ExpressionType e)
431 {
432 switch (e)
433 {
434 case ExpressionType.LessThan:
435 return ExpressionType.GreaterThan;
436 case ExpressionType.LessThanOrEqual:
437 return ExpressionType.GreaterThanOrEqual;
438 case ExpressionType.GreaterThan:
439 return ExpressionType.LessThan;
440 case ExpressionType.GreaterThanOrEqual:
441 return ExpressionType.LessThanOrEqual;
442 default:
443 return e;
444 }
445 }
446
447 private static bool IsIntegerType(Type t)
448 {
449 return
450 t == typeof(sbyte) ||
451 t == typeof(byte) ||
452 t == typeof(short) ||
453 t == typeof(ushort) ||
454 t == typeof(int) ||
455 t == typeof(uint) ||
456 t == typeof(long) ||
457 t == typeof(ulong);
458 }
459
460 private void ParseQueryCondition(
461 BinaryExpression binaryExpression, StringBuilder queryBuilder)
462 {
463 bool swap;
464 string column = this.GetConditionColumn(binaryExpression, out swap);
465 queryBuilder.Append(column);
466
467 ExpressionType expressionType = binaryExpression.NodeType;
468 if (swap)
469 {
470 expressionType = OppositeExpression(expressionType);
471 }
472
473 LambdaExpression valueExpression = Expression.Lambda(
474 swap ? binaryExpression.Left : binaryExpression.Right);
475 object value = valueExpression.Compile().DynamicInvoke();
476
477 bool valueIsInt = false;
478 if (value != null)
479 {
480 if (IsIntegerType(value.GetType()))
481 {
482 valueIsInt = true;
483 }
484 else
485 {
486 value = value.ToString();
487 }
488 }
489
490 switch (expressionType)
491 {
492 case ExpressionType.Equal:
493 if (value == null)
494 {
495 queryBuilder.Append(" IS NULL");
496 }
497 else if (valueIsInt)
498 {
499 queryBuilder.Append(" = ");
500 queryBuilder.Append(value);
501 }
502 else
503 {
504 queryBuilder.Append(" = ?");
505 this.whereParameters.Add(value);
506 }
507 return;
508
509 case ExpressionType.NotEqual:
510 if (value == null)
511 {
512 queryBuilder.Append(" IS NOT NULL");
513 }
514 else if (valueIsInt)
515 {
516 queryBuilder.Append(" <> ");
517 queryBuilder.Append(value);
518 }
519 else
520 {
521 queryBuilder.Append(" <> ?");
522 this.whereParameters.Add(value);
523 }
524 return;
525 }
526
527 if (value == null)
528 {
529 throw new InvalidOperationException(
530 "A null value was used in a greater-than/less-than operation.");
531 }
532
533 if (!valueIsInt)
534 {
535 throw new NotSupportedException(
536 "Greater-than/less-than operators not supported on strings.");
537 }
538
539 switch (expressionType)
540 {
541 case ExpressionType.LessThan:
542 queryBuilder.Append(" < ");
543 break;
544
545 case ExpressionType.LessThanOrEqual:
546 queryBuilder.Append(" <= ");
547 break;
548
549 case ExpressionType.GreaterThan:
550 queryBuilder.Append(" > ");
551 break;
552
553 case ExpressionType.GreaterThanOrEqual:
554 queryBuilder.Append(" >= ");
555 break;
556
557 default:
558 throw new NotSupportedException(
559 "Unsupported query expression type: " + expressionType);
560 }
561
562 queryBuilder.Append(value);
563 }
564
565 private string GetConditionColumn(
566 BinaryExpression binaryExpression, out bool swap)
567 {
568 MemberExpression memberExpression;
569 MethodCallExpression methodCallExpression;
570
571 if (((memberExpression = binaryExpression.Left as MemberExpression) != null) ||
572 ((binaryExpression.Left.NodeType == ExpressionType.Convert ||
573 binaryExpression.Left.NodeType == ExpressionType.ConvertChecked) &&
574 (memberExpression = ((UnaryExpression) binaryExpression.Left).Operand
575 as MemberExpression) != null))
576 {
577 string column = this.GetConditionColumn(memberExpression);
578 if (column != null)
579 {
580 swap = false;
581 return column;
582 }
583 }
584 else if (((memberExpression = binaryExpression.Right as MemberExpression) != null) ||
585 ((binaryExpression.Right.NodeType == ExpressionType.Convert ||
586 binaryExpression.Right.NodeType == ExpressionType.ConvertChecked) &&
587 (memberExpression = ((UnaryExpression) binaryExpression.Right).Operand
588 as MemberExpression) != null))
589 {
590 string column = this.GetConditionColumn(memberExpression);
591 if (column != null)
592 {
593 swap = true;
594 return column;
595 }
596 }
597 else if ((methodCallExpression = binaryExpression.Left as MethodCallExpression) != null)
598 {
599 string column = this.GetConditionColumn(methodCallExpression);
600 if (column != null)
601 {
602 swap = false;
603 return column;
604 }
605 }
606 else if ((methodCallExpression = binaryExpression.Right as MethodCallExpression) != null)
607 {
608 string column = this.GetConditionColumn(methodCallExpression);
609 if (column != null)
610 {
611 swap = true;
612 return column;
613 }
614 }
615
616 throw new NotSupportedException(
617 "Unsupported binary expression: " + binaryExpression);
618 }
619
620 private string GetConditionColumn(MemberExpression memberExpression)
621 {
622 string columnName = GetColumnName(memberExpression.Member);
623 string selectorName = GetConditionSelectorName(memberExpression.Expression);
624 string tableName = this.GetConditionTable(selectorName, columnName);
625 return this.FormatColumn(tableName, columnName);
626 }
627
628 private string GetConditionColumn(MethodCallExpression methodCallExpression)
629 {
630 LambdaExpression argumentExpression =
631 Expression.Lambda(methodCallExpression.Arguments[0]);
632 string columnName = (string) argumentExpression.Compile().DynamicInvoke();
633 string selectorName = GetConditionSelectorName(methodCallExpression.Object);
634 string tableName = this.GetConditionTable(selectorName, columnName);
635 return this.FormatColumn(tableName, columnName);
636 }
637
638 private static string GetConditionSelectorName(Expression expression)
639 {
640 ParameterExpression parameterExpression;
641 MemberExpression memberExpression;
642 if ((parameterExpression = expression as ParameterExpression) != null)
643 {
644 return parameterExpression.Name;
645 }
646 else if ((memberExpression = expression as MemberExpression) != null)
647 {
648 return memberExpression.Member.Name;
649 }
650 else
651 {
652 throw new NotSupportedException(
653 "Unsupported conditional selector expression: " + expression);
654 }
655 }
656
657 private string GetConditionTable(string selectorName, string columnName)
658 {
659 string tableName = null;
660
661 for (int i = 0; i < this.tables.Count; i++)
662 {
663 if (this.selectors[i] == selectorName)
664 {
665 tableName = this.tables[i].Name;
666 break;
667 }
668 }
669
670 if (tableName == null)
671 {
672 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture,
673 "Conditional expression contains column {0}.{1} " +
674 "from a table that is not in the query.",
675 selectorName,
676 columnName));
677 }
678
679 return tableName;
680 }
681
682 private string FormatColumn(string tableName, string columnName)
683 {
684 if (tableName != null && this.tables.Count > 1)
685 {
686 return String.Format(CultureInfo.InvariantCulture, "`{0}`.`{1}`", tableName, columnName);
687 }
688 else
689 {
690 return String.Format(CultureInfo.InvariantCulture, "`{0}`", columnName);
691 }
692 }
693
694 private static string GetColumnName(MemberInfo memberInfo)
695 {
696 foreach (var attr in memberInfo.GetCustomAttributes(
697 typeof(DatabaseColumnAttribute), false))
698 {
699 return ((DatabaseColumnAttribute) attr).Column;
700 }
701
702 return memberInfo.Name;
703 }
704
705 internal void BuildProjection(TableInfo tableInfo, LambdaExpression expression)
706 {
707 if (tableInfo != null)
708 {
709 this.tables.Add(tableInfo);
710 this.recordTypes.Add(typeof(T));
711 this.selectors.Add(expression.Parameters[0].Name);
712 }
713
714 this.FindColumns(expression, this.selectColumns);
715 this.projectionDelegates.Add(expression.Compile());
716 }
717
718 internal void BuildSequence(TableInfo tableInfo, LambdaExpression expression)
719 {
720 if (tableInfo != null)
721 {
722 this.tables.Add(tableInfo);
723 this.recordTypes.Add(typeof(T));
724 this.selectors.Add(expression.Parameters[0].Name);
725 }
726
727 this.FindColumns(expression.Body, this.orderbyColumns);
728 }
729
730 private static void AddAllColumns(TableInfo tableInfo, IList<TableColumn> columnList)
731 {
732 foreach (ColumnInfo column in tableInfo.Columns)
733 {
734 columnList.Add(new TableColumn(tableInfo, column));
735 }
736 }
737
738 [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
739 private void FindColumns(Expression expression, IList<TableColumn> columnList)
740 {
741 if (expression is ParameterExpression)
742 {
743 ParameterExpression e = expression as ParameterExpression;
744 string selector = e.Name;
745 for (int i = 0; i < this.tables.Count; i++)
746 {
747 if (this.selectors[i] == selector)
748 {
749 AddAllColumns(this.tables[i], columnList);
750 break;
751 }
752 }
753 }
754 else if (expression.NodeType == ExpressionType.MemberAccess)
755 {
756 this.FindColumns(expression as MemberExpression, columnList);
757 }
758 else if (expression is MethodCallExpression)
759 {
760 this.FindColumns(expression as MethodCallExpression, columnList);
761 }
762 else if (expression is BinaryExpression)
763 {
764 BinaryExpression e = expression as BinaryExpression;
765 this.FindColumns(e.Left, columnList);
766 this.FindColumns(e.Right, columnList);
767 }
768 else if (expression is UnaryExpression)
769 {
770 UnaryExpression e = expression as UnaryExpression;
771 this.FindColumns(e.Operand, columnList);
772 }
773 else if (expression is ConditionalExpression)
774 {
775 ConditionalExpression e = expression as ConditionalExpression;
776 this.FindColumns(e.Test, columnList);
777 this.FindColumns(e.IfTrue, columnList);
778 this.FindColumns(e.IfFalse, columnList);
779 }
780 else if (expression is InvocationExpression)
781 {
782 InvocationExpression e = expression as InvocationExpression;
783 this.FindColumns(e.Expression, columnList);
784 this.FindColumns(e.Arguments, columnList);
785 }
786 else if (expression is LambdaExpression)
787 {
788 LambdaExpression e = expression as LambdaExpression;
789 this.FindColumns(e.Body, columnList);
790 }
791 else if (expression is ListInitExpression)
792 {
793 ListInitExpression e = expression as ListInitExpression;
794 this.FindColumns(e.NewExpression, columnList);
795 foreach (ElementInit ei in e.Initializers)
796 {
797 this.FindColumns(ei.Arguments, columnList);
798 }
799 }
800 else if (expression is MemberInitExpression)
801 {
802 MemberInitExpression e = expression as MemberInitExpression;
803 this.FindColumns(e.NewExpression, columnList);
804 foreach (MemberAssignment b in e.Bindings)
805 {
806 this.FindColumns(b.Expression, columnList);
807 }
808 }
809 else if (expression is NewExpression)
810 {
811 NewExpression e = expression as NewExpression;
812 this.FindColumns(e.Arguments, columnList);
813 }
814 else if (expression is NewArrayExpression)
815 {
816 NewArrayExpression e = expression as NewArrayExpression;
817 this.FindColumns(e.Expressions, columnList);
818 }
819 else if (expression is TypeBinaryExpression)
820 {
821 TypeBinaryExpression e = expression as TypeBinaryExpression;
822 this.FindColumns(e.Expression, columnList);
823 }
824 }
825
826 private void FindColumns(IEnumerable<Expression> expressions, IList<TableColumn> columnList)
827 {
828 foreach (Expression expression in expressions)
829 {
830 this.FindColumns(expression, columnList);
831 }
832 }
833
834 private void FindColumns(MemberExpression memberExpression, IList<TableColumn> columnList)
835 {
836 string selector = null;
837 MemberExpression objectMemberExpression;
838 ParameterExpression objectParameterExpression;
839 if ((objectParameterExpression = memberExpression.Expression as
840 ParameterExpression) != null)
841 {
842 selector = objectParameterExpression.Name;
843 }
844 else if ((objectMemberExpression = memberExpression.Expression as
845 MemberExpression) != null)
846 {
847 selector = objectMemberExpression.Member.Name;
848 }
849
850 if (selector != null)
851 {
852 for (int i = 0; i < this.tables.Count; i++)
853 {
854 if (this.selectors[i] == selector)
855 {
856 string columnName = GetColumnName(memberExpression.Member);
857 ColumnInfo column = this.tables[i].Columns[columnName];
858 columnList.Add(new TableColumn(this.tables[i], column));
859 break;
860 }
861 }
862 }
863
864 selector = memberExpression.Member.Name;
865 for (int i = 0; i < this.tables.Count; i++)
866 {
867 if (this.selectors[i] == selector)
868 {
869 AddAllColumns(this.tables[i], columnList);
870 break;
871 }
872 }
873 }
874
875 private void FindColumns(MethodCallExpression methodCallExpression, IList<TableColumn> columnList)
876 {
877 if (methodCallExpression.Method.Name == "get_Item" &&
878 methodCallExpression.Arguments.Count == 1 &&
879 methodCallExpression.Arguments[0].Type == typeof(string))
880 {
881 string selector = null;
882 MemberExpression objectMemberExpression;
883 ParameterExpression objectParameterExpression;
884 if ((objectParameterExpression = methodCallExpression.Object as ParameterExpression) != null)
885 {
886 selector = objectParameterExpression.Name;
887 }
888 else if ((objectMemberExpression = methodCallExpression.Object as MemberExpression) != null)
889 {
890 selector = objectMemberExpression.Member.Name;
891 }
892
893 if (selector != null)
894 {
895 for (int i = 0; i < this.tables.Count; i++)
896 {
897 if (this.selectors[i] == selector)
898 {
899 LambdaExpression argumentExpression =
900 Expression.Lambda(methodCallExpression.Arguments[0]);
901 string columnName = (string)
902 argumentExpression.Compile().DynamicInvoke();
903 ColumnInfo column = this.tables[i].Columns[columnName];
904 columnList.Add(new TableColumn(this.tables[i], column));
905 break;
906 }
907 }
908 }
909 }
910
911 if (methodCallExpression.Object != null && methodCallExpression.Object.NodeType != ExpressionType.Parameter)
912 {
913 this.FindColumns(methodCallExpression.Object, columnList);
914 }
915 }
916
917 internal void PerformJoin(
918 TableInfo tableInfo,
919 Type recordType,
920 IQueryable joinTable,
921 LambdaExpression outerKeySelector,
922 LambdaExpression innerKeySelector,
923 LambdaExpression resultSelector)
924 {
925 if (joinTable == null)
926 {
927 throw new ArgumentNullException("joinTable");
928 }
929
930 if (tableInfo != null)
931 {
932 this.tables.Add(tableInfo);
933 this.recordTypes.Add(recordType);
934 this.selectors.Add(outerKeySelector.Parameters[0].Name);
935 }
936
937 PropertyInfo tableInfoProp = joinTable.GetType().GetProperty("TableInfo");
938 if (tableInfoProp == null)
939 {
940 throw new NotSupportedException(
941 "Cannot join with object: " + joinTable.GetType().Name +
942 "; join is only supported on another QTable.");
943 }
944
945 TableInfo joinTableInfo = (TableInfo) tableInfoProp.GetValue(joinTable, null);
946 if (joinTableInfo == null)
947 {
948 throw new InvalidOperationException("Missing join table info.");
949 }
950
951 this.tables.Add(joinTableInfo);
952 this.recordTypes.Add(joinTable.ElementType);
953 this.selectors.Add(innerKeySelector.Parameters[0].Name);
954 this.projectionDelegates.Add(resultSelector.Compile());
955
956 int joinColumnCount = this.joinColumns.Count;
957 this.FindColumns(outerKeySelector.Body, this.joinColumns);
958 if (this.joinColumns.Count > joinColumnCount + 1)
959 {
960 throw new NotSupportedException("Join operations involving " +
961 "multiple columns are not supported.");
962 }
963 else if (this.joinColumns.Count != joinColumnCount + 1)
964 {
965 throw new InvalidOperationException("Bad outer key selector for join.");
966 }
967
968 this.FindColumns(innerKeySelector.Body, this.joinColumns);
969 if (this.joinColumns.Count > joinColumnCount + 2)
970 {
971 throw new NotSupportedException("Join operations involving " +
972 "multiple columns not are supported.");
973 }
974 if (this.joinColumns.Count != joinColumnCount + 2)
975 {
976 throw new InvalidOperationException("Bad inner key selector for join.");
977 }
978 }
979 }
980
981 internal class TableColumn
982 {
983 public TableColumn(TableInfo table, ColumnInfo column)
984 {
985 this.Table = table;
986 this.Column = column;
987 }
988
989 public TableInfo Table { get; set; }
990 public ColumnInfo Column { get; set; }
991 }
992}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj
new file mode 100644
index 00000000..b4587071
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj
@@ -0,0 +1,21 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- 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. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.WindowsInstaller.Linq</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.WindowsInstaller.Linq</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net35</TargetFrameworks>
9 <Description>LINQ extensions for Windows Installer classes</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
19 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
20 </ItemGroup>
21</Project>