New tests.
[mono.git] / mcs / class / System.Data.Linq / src / DbLinq / Vendor / Implementation / SchemaLoader.cs
1 #region MIT license\r
2 // \r
3 // MIT license\r
4 //\r
5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne\r
6 // \r
7 // Permission is hereby granted, free of charge, to any person obtaining a copy\r
8 // of this software and associated documentation files (the "Software"), to deal\r
9 // in the Software without restriction, including without limitation the rights\r
10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
11 // copies of the Software, and to permit persons to whom the Software is\r
12 // furnished to do so, subject to the following conditions:\r
13 // \r
14 // The above copyright notice and this permission notice shall be included in\r
15 // all copies or substantial portions of the Software.\r
16 // \r
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
23 // THE SOFTWARE.\r
24 // \r
25 #endregion\r
26 using System;\r
27 using System.Collections.Generic;\r
28 using System.Data;\r
29 using System.IO;\r
30 using System.Linq;\r
31 \r
32 #if MONO_STRICT\r
33 using System.Data.Linq;\r
34 #else\r
35 using DbLinq.Data.Linq;\r
36 #endif\r
37 \r
38 using DbLinq.Factory;\r
39 using DbLinq.Schema;\r
40 using DbLinq.Schema.Dbml;\r
41 using System.Text.RegularExpressions;\r
42 \r
43 namespace DbLinq.Vendor.Implementation\r
44 {\r
45 #if !MONO_STRICT\r
46     public\r
47 #endif\r
48     abstract partial class SchemaLoader : ISchemaLoader\r
49     {\r
50         /// <summary>\r
51         /// Underlying vendor\r
52         /// </summary>\r
53         /// <value></value>\r
54         public abstract IVendor Vendor { get; set; }\r
55         /// <summary>\r
56         /// Connection used to read schema\r
57         /// </summary>\r
58         /// <value></value>\r
59         public IDbConnection Connection { get; set; }\r
60         /// <summary>\r
61         /// Gets or sets the name formatter.\r
62         /// </summary>\r
63         /// <value>The name formatter.</value>\r
64         public INameFormatter NameFormatter { get; set; }\r
65 \r
66         private TextWriter log;\r
67         /// <summary>\r
68         /// Log output\r
69         /// </summary>\r
70         public TextWriter Log\r
71         {\r
72             get { return log ?? Console.Out; }\r
73             set { log = value; }\r
74         }\r
75 \r
76         /// <summary>\r
77         /// Loads database schema\r
78         /// </summary>\r
79         /// <param name="databaseName"></param>\r
80         /// <param name="nameAliases"></param>\r
81         /// <param name="nameFormat"></param>\r
82         /// <param name="loadStoredProcedures"></param>\r
83         /// <param name="contextNamespace"></param>\r
84         /// <param name="entityNamespace"></param>\r
85         /// <returns></returns>\r
86         public virtual Database Load(string databaseName, INameAliases nameAliases, NameFormat nameFormat,\r
87             bool loadStoredProcedures, string contextNamespace, string entityNamespace)\r
88         {\r
89             // check if connection is open. Note: we may use something more flexible\r
90             if (Connection.State != ConnectionState.Open)\r
91                 Connection.Open();\r
92 \r
93             // get the database name. If we don't have one, take it from connection string...\r
94             if (string.IsNullOrEmpty(databaseName))\r
95                 databaseName = Connection.Database;\r
96             // ... and if connection string doesn't provide a name, then throw an error\r
97             if (string.IsNullOrEmpty(databaseName))\r
98                 throw new ArgumentException("A database name is required. Please specify /database=<databaseName>");\r
99 \r
100             databaseName = GetDatabaseNameAliased(databaseName, nameAliases);\r
101 \r
102             var schemaName = NameFormatter.GetSchemaName(databaseName, GetExtraction(databaseName), nameFormat);\r
103             var names = new Names();\r
104             var schema = new Database\r
105                              {\r
106                                  Name = schemaName.DbName,\r
107                                  Class = GetRuntimeClassName(schemaName.ClassName, nameAliases),\r
108                                  BaseType = typeof(DataContext).FullName,\r
109                                  ContextNamespace = contextNamespace,\r
110                                  EntityNamespace = entityNamespace,\r
111                              };\r
112 \r
113             // order is important, we must have:\r
114             // 1. tables\r
115             // 2. columns\r
116             // 3. constraints\r
117             LoadTables(schema, schemaName, Connection, nameAliases, nameFormat, names);\r
118             LoadColumns(schema, schemaName, Connection, nameAliases, nameFormat, names);\r
119             CheckColumnsName(schema);\r
120             LoadConstraints(schema, schemaName, Connection, nameFormat, names);\r
121             CheckConstraintsName(schema);\r
122             if (loadStoredProcedures)\r
123                 LoadStoredProcedures(schema, schemaName, Connection, nameFormat);\r
124             // names aren't checked here anymore, because this confuses DBML editor.\r
125             // they will (for now) be checked before .cs generation\r
126             // in the end, when probably will end up in mapping source (or somewhere around)\r
127             //CheckNamesSafety(schema);\r
128 \r
129             // generate backing fields name (since we have here correct names)\r
130             GenerateStorageAndMemberFields(schema);\r
131 \r
132             return schema;\r
133         }\r
134 \r
135         /// <summary>\r
136         /// Gets a usable name for the database.\r
137         /// </summary>\r
138         /// <param name="databaseName">Name of the database.</param>\r
139         /// <returns></returns>\r
140         protected virtual string GetDatabaseName(string databaseName)\r
141         {\r
142             return databaseName;\r
143         }\r
144 \r
145         protected virtual string GetDatabaseNameAliased(string databaseName, INameAliases nameAliases)\r
146         {\r
147             string databaseNameAliased = nameAliases != null ? nameAliases.GetDatabaseNameAlias(databaseName) : null;\r
148             return (databaseNameAliased != null) ? databaseNameAliased : GetDatabaseName(databaseName);\r
149         }\r
150 \r
151         /// <summary>\r
152         /// Gets a usable name for the database class.\r
153         /// </summary>\r
154         /// <param name="databaseName">Name of the clas.</param>\r
155         /// <returns></returns>\r
156         protected virtual string GetRuntimeClassName(string className, INameAliases nameAliases)\r
157         {\r
158             string classNameAliased = nameAliases != null ? nameAliases.GetClassNameAlias(className) : null;\r
159             return (classNameAliased != null) ? classNameAliased : className;\r
160         }\r
161 \r
162         /// <summary>\r
163         /// Writes an error line.\r
164         /// </summary>\r
165         /// <param name="format">The format.</param>\r
166         /// <param name="arg">The arg.</param>\r
167         protected void WriteErrorLine(string format, params object[] arg)\r
168         {\r
169             var o = Log;\r
170             if (o == Console.Out)\r
171                 o = Console.Error;\r
172             o.WriteLine(format, arg);\r
173         }\r
174 \r
175         protected SchemaLoader()\r
176         {\r
177             NameFormatter = ObjectFactory.Create<INameFormatter>(); // the Pluralize property is set dynamically, so no singleton\r
178         }\r
179 \r
180         /// <summary>\r
181         /// Gets the extraction type from a columnname.\r
182         /// </summary>\r
183         /// <param name="dbColumnName">Name of the db column.</param>\r
184         /// <returns></returns>\r
185         protected virtual WordsExtraction GetExtraction(string dbColumnName)\r
186         {\r
187             bool isMixedCase = dbColumnName != dbColumnName.ToLower() && dbColumnName != dbColumnName.ToUpper();\r
188             return isMixedCase ? WordsExtraction.FromCase : WordsExtraction.FromDictionary;\r
189         }\r
190 \r
191         /// <summary>\r
192         /// Gets the full name of a name and schema.\r
193         /// </summary>\r
194         /// <param name="dbName">Name of the db.</param>\r
195         /// <param name="dbSchema">The db schema.</param>\r
196         /// <returns></returns>\r
197         protected virtual string GetFullDbName(string dbName, string dbSchema)\r
198         {\r
199             string fullDbName;\r
200             if (dbSchema == null)\r
201                 fullDbName = dbName;\r
202             else\r
203                 fullDbName = string.Format("{0}.{1}", dbSchema, dbName);\r
204             return fullDbName;\r
205         }\r
206 \r
207         /// <summary>\r
208         /// Creates the name of the table given a name and schema\r
209         /// </summary>\r
210         /// <param name="dbTableName">Name of the db table.</param>\r
211         /// <param name="dbSchema">The db schema.</param>\r
212         /// <param name="nameAliases">The name aliases.</param>\r
213         /// <param name="nameFormat">The name format.</param>\r
214         /// <param name="extraction">The extraction.</param>\r
215         /// <returns></returns>\r
216         protected virtual TableName CreateTableName(string dbTableName, string dbSchema, INameAliases nameAliases, NameFormat nameFormat, WordsExtraction extraction)\r
217         {\r
218             // if we have an alias, use it, and don't try to analyze it (a human probably already did the job)\r
219             var tableTypeAlias = nameAliases != null ? nameAliases.GetTableTypeAlias(dbTableName, dbSchema) : null;\r
220             if (tableTypeAlias != null)\r
221                 extraction = WordsExtraction.None;\r
222             else\r
223                 tableTypeAlias = dbTableName;\r
224 \r
225             var tableName = NameFormatter.GetTableName(tableTypeAlias, extraction, nameFormat);\r
226 \r
227             // alias for member\r
228             var tableMemberAlias = nameAliases != null ? nameAliases.GetTableMemberAlias(dbTableName, dbSchema) : null;\r
229             if (tableMemberAlias != null)\r
230                 tableName.MemberName = tableMemberAlias;\r
231 \r
232             tableName.DbName = GetFullDbName(dbTableName, dbSchema);\r
233             return tableName;\r
234         }\r
235 \r
236         protected virtual TableName CreateTableName(string dbTableName, string dbSchema, INameAliases nameAliases, NameFormat nameFormat)\r
237         {\r
238             return CreateTableName(dbTableName, dbSchema, nameAliases, nameFormat, GetExtraction(dbTableName));\r
239         }\r
240 \r
241         Regex startsWithNumber = new Regex(@"^\d", RegexOptions.Compiled);\r
242 \r
243         /// <summary>\r
244         /// Creates the name of the column.\r
245         /// </summary>\r
246         /// <param name="dbColumnName">Name of the db column.</param>\r
247         /// <param name="dbTableName">Name of the db table.</param>\r
248         /// <param name="dbSchema">The db schema.</param>\r
249         /// <param name="nameAliases">The name aliases.</param>\r
250         /// <param name="nameFormat">The name format.</param>\r
251         /// <returns></returns>\r
252         protected virtual ColumnName CreateColumnName(string dbColumnName, string dbTableName, string dbSchema, INameAliases nameAliases, NameFormat nameFormat)\r
253         {\r
254             var columnNameAlias = nameAliases != null ? nameAliases.GetColumnMemberAlias(dbColumnName, dbTableName, dbSchema) : null;\r
255             WordsExtraction extraction;\r
256             if (columnNameAlias != null)\r
257             {\r
258                 extraction = WordsExtraction.None;\r
259             }\r
260             else\r
261             {\r
262                 extraction = GetExtraction(dbColumnName);\r
263                 columnNameAlias = dbColumnName;\r
264             }\r
265             var columnName = NameFormatter.GetColumnName(columnNameAlias, extraction, nameFormat);\r
266             // The member name can not be the same as the class\r
267             // we add a "1" (just like SqlMetal does)\r
268             var tableName = CreateTableName(dbTableName, dbSchema, nameAliases, nameFormat);\r
269             if (columnName.PropertyName == tableName.ClassName)\r
270                 columnName.PropertyName = columnName.PropertyName + "1";\r
271 \r
272             if (startsWithNumber.IsMatch(columnName.PropertyName))\r
273                 columnName.PropertyName = "_" + columnName.PropertyName;\r
274 \r
275             columnName.DbName = dbColumnName;\r
276             return columnName;\r
277         }\r
278 \r
279         /// <summary>\r
280         /// Creates the name of the procedure.\r
281         /// </summary>\r
282         /// <param name="dbProcedureName">Name of the db procedure.</param>\r
283         /// <param name="dbSchema">The db schema.</param>\r
284         /// <param name="nameFormat">The name format.</param>\r
285         /// <returns></returns>\r
286         protected virtual ProcedureName CreateProcedureName(string dbProcedureName, string dbSchema, NameFormat nameFormat)\r
287         {\r
288             var procedureName = NameFormatter.GetProcedureName(dbProcedureName, GetExtraction(dbProcedureName), nameFormat);\r
289             procedureName.DbName = GetFullDbName(dbProcedureName, dbSchema);\r
290             return procedureName;\r
291         }\r
292 \r
293         /// <summary>\r
294         /// Creates the name of the association.\r
295         /// </summary>\r
296         /// <param name="dbManyName">Name of the db many.</param>\r
297         /// <param name="dbManySchema">The db many schema.</param>\r
298         /// <param name="dbOneName">Name of the db one.</param>\r
299         /// <param name="dbOneSchema">The db one schema.</param>\r
300         /// <param name="dbConstraintName">Name of the db constraint.</param>\r
301         /// <param name="foreignKeyName">Name of the foreign key.</param>\r
302         /// <param name="nameFormat">The name format.</param>\r
303         /// <returns></returns>\r
304         protected virtual AssociationName CreateAssociationName(string dbManyName, string dbManySchema,\r
305             string dbOneName, string dbOneSchema, string dbConstraintName, string foreignKeyName, NameFormat nameFormat)\r
306         {\r
307             var associationName = NameFormatter.GetAssociationName(dbManyName, dbOneName,\r
308                 dbConstraintName, foreignKeyName, GetExtraction(dbManyName), nameFormat);\r
309             associationName.DbName = GetFullDbName(dbManyName, dbManySchema);\r
310             return associationName;\r
311         }\r
312 \r
313         /// <summary>\r
314         /// Creates the name of the schema.\r
315         /// </summary>\r
316         /// <param name="databaseName">Name of the database.</param>\r
317         /// <param name="connection">The connection.</param>\r
318         /// <param name="nameFormat">The name format.</param>\r
319         /// <returns></returns>\r
320         protected virtual SchemaName CreateSchemaName(string databaseName, IDbConnection connection, NameFormat nameFormat)\r
321         {\r
322             if (string.IsNullOrEmpty(databaseName))\r
323             {\r
324                 databaseName = connection.Database;\r
325                 if (string.IsNullOrEmpty(databaseName))\r
326                     throw new ArgumentException("Could not deduce database name from connection string. Please specify /database=<databaseName>");\r
327             }\r
328             return NameFormatter.GetSchemaName(databaseName, GetExtraction(databaseName), nameFormat);\r
329         }\r
330 \r
331         protected virtual ParameterName CreateParameterName(string dbParameterName, NameFormat nameFormat)\r
332         {\r
333             var parameterName = NameFormatter.GetParameterName(dbParameterName, GetExtraction(dbParameterName), nameFormat);\r
334             return parameterName;\r
335         }\r
336 \r
337         protected class Names\r
338         {\r
339             public IDictionary<string, TableName> TablesNames = new Dictionary<string, TableName>();\r
340             public IDictionary<string, IDictionary<string, ColumnName>> ColumnsNames = new Dictionary<string, IDictionary<string, ColumnName>>();\r
341 \r
342             public void AddColumn(string dbTableName, ColumnName columnName)\r
343             {\r
344                 IDictionary<string, ColumnName> columns;\r
345                 if (!ColumnsNames.TryGetValue(dbTableName, out columns))\r
346                 {\r
347                     columns = new Dictionary<string, ColumnName>();\r
348                     ColumnsNames[dbTableName] = columns;\r
349                 }\r
350                 columns[columnName.DbName] = columnName;\r
351             }\r
352         }\r
353 \r
354         /// <summary>\r
355         /// Loads the tables in the given schema.\r
356         /// </summary>\r
357         /// <param name="schema">The schema.</param>\r
358         /// <param name="schemaName">Name of the schema.</param>\r
359         /// <param name="conn">The conn.</param>\r
360         /// <param name="nameAliases">The name aliases.</param>\r
361         /// <param name="nameFormat">The name format.</param>\r
362         /// <param name="names">The names.</param>\r
363         protected virtual void LoadTables(Database schema, SchemaName schemaName, IDbConnection conn, INameAliases nameAliases, NameFormat nameFormat, Names names)\r
364         {\r
365             var tables = ReadTables(conn, schemaName.DbName);\r
366             foreach (var row in tables)\r
367             {\r
368                 var tableName = CreateTableName(row.Name, row.Schema, nameAliases, nameFormat);\r
369                 names.TablesNames[tableName.DbName] = tableName;\r
370 \r
371                 var table = new Table();\r
372                 table.Name = tableName.DbName;\r
373                 table.Member = tableName.MemberName;\r
374                 table.Type.Name = tableName.ClassName;\r
375                 schema.Tables.Add(table);\r
376             }\r
377         }\r
378 \r
379         /// <summary>\r
380         /// Loads the columns.\r
381         /// </summary>\r
382         /// <param name="schema">The schema.</param>\r
383         /// <param name="schemaName">Name of the schema.</param>\r
384         /// <param name="conn">The conn.</param>\r
385         /// <param name="nameAliases">The name aliases.</param>\r
386         /// <param name="nameFormat">The name format.</param>\r
387         /// <param name="names">The names.</param>\r
388         protected void LoadColumns(Database schema, SchemaName schemaName, IDbConnection conn, INameAliases nameAliases, NameFormat nameFormat, Names names)\r
389         {\r
390             var columnRows = ReadColumns(conn, schemaName.DbName);\r
391             foreach (var columnRow in columnRows)\r
392             {\r
393                 var columnName = CreateColumnName(columnRow.ColumnName, columnRow.TableName, columnRow.TableSchema, nameAliases, nameFormat);\r
394                 names.AddColumn(columnRow.TableName, columnName);\r
395 \r
396                 //find which table this column belongs to\r
397                 string fullColumnDbName = GetFullDbName(columnRow.TableName, columnRow.TableSchema);\r
398                 DbLinq.Schema.Dbml.Table tableSchema = schema.Tables.FirstOrDefault(tblSchema => fullColumnDbName == tblSchema.Name);\r
399                 if (tableSchema == null)\r
400                 {\r
401                     WriteErrorLine("ERROR L46: Table '" + columnRow.TableName + "' not found for column " + columnRow.ColumnName);\r
402                     continue;\r
403                 }\r
404                 var column = new Column();\r
405                 column.Name = columnName.DbName;\r
406                 column.Member = columnName.PropertyName;\r
407                 column.DbType = columnRow.FullType;\r
408 \r
409                 if (columnRow.PrimaryKey.HasValue)\r
410                     column.IsPrimaryKey = columnRow.PrimaryKey.Value;\r
411 \r
412                 bool? generated = (nameAliases != null) ? nameAliases.GetColumnGenerated(columnRow.ColumnName, columnRow.TableName, columnRow.TableSchema) : null;\r
413                 if (!generated.HasValue)\r
414                     generated = columnRow.Generated;\r
415                 if (generated.HasValue)\r
416                     column.IsDbGenerated = generated.Value;\r
417 \r
418                 AutoSync? autoSync = (nameAliases != null) ? nameAliases.GetColumnAutoSync(columnRow.ColumnName, columnRow.TableName, columnRow.TableSchema) : null;\r
419                 if (autoSync.HasValue)\r
420                     column.AutoSync = autoSync.Value;\r
421 \r
422                 // the Expression can originate from two sources:\r
423                 // 1. DefaultValue\r
424                 // 2. Expression\r
425                 // we use any valid source (we can't have both)\r
426                 if (column.IsDbGenerated && columnRow.DefaultValue != null)\r
427                     column.Expression = columnRow.DefaultValue;\r
428 \r
429                 column.CanBeNull = columnRow.Nullable;\r
430 \r
431                 string columnTypeAlias = nameAliases != null ? nameAliases.GetColumnForcedType(columnRow.ColumnName, columnRow.TableName, columnRow.TableSchema) : null;\r
432                 var columnType = MapDbType(columnName.DbName, columnRow);\r
433 \r
434                 var columnEnumType = columnType as EnumType;\r
435                 if (columnEnumType != null)\r
436                 {\r
437                     var enumType = column.SetExtendedTypeAsEnumType();\r
438                     enumType.Name = columnEnumType.Name;\r
439                     foreach (KeyValuePair<string, int> enumValue in columnEnumType.EnumValues)\r
440                     {\r
441                         enumType[enumValue.Key] = enumValue.Value;\r
442                     }\r
443                 }\r
444                 else if (columnTypeAlias != null)\r
445                     column.Type = columnTypeAlias;\r
446                 else\r
447                     column.Type = columnType.ToString();\r
448 \r
449                 tableSchema.Type.Columns.Add(column);\r
450             }\r
451         }\r
452     }\r
453 }\r