PageRenderTime 49ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/1.Framework/Vita.Data/Management/DbModelComparer.cs

http://vita.codeplex.com
C# | 458 lines | 379 code | 41 blank | 38 comment | 178 complexity | e24c3c09a6c5a228145d59559bffb299 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Vita.Common;
  6. using Vita.Entities;
  7. using Vita.Entities.Model;
  8. using Vita.Logging;
  9. using Vita.Data.Driver;
  10. using Vita.Data.Model;
  11. using Vita.Data.Runtime;
  12. namespace Vita.Data.Management {
  13. public class DbModelComparer {
  14. DbSchemaUpdateInfo _changes;
  15. DbModel _newModel, _oldModel;
  16. DbSchemaUpdateOptions _optons;
  17. bool _useRefIntegrity, _useStoredProcs, _deleteTables, _supportsSchemas, _supportsClusteredIndexes;
  18. Log _log;
  19. public DbSchemaUpdateInfo GetSchemaChanges(DbModel newModel, DbModel oldModel, Log log, DbSchemaUpdateOptions updateOptions) {
  20. _newModel = newModel;
  21. _oldModel = oldModel;
  22. _log = log;
  23. _optons = updateOptions;
  24. _changes = new DbSchemaUpdateInfo(_newModel, _oldModel);
  25. _changes.Actions.Clear();
  26. var driver = _newModel.Driver;
  27. _useRefIntegrity = driver.Supports(DbCapabilities.ReferentialConstraints) && _newModel.Settings.Options.IsSet(DbOptions.UseRefIntegrity) ;
  28. _useStoredProcs = driver.Supports(DbCapabilities.StoredProcedures) && _newModel.Settings.Options.IsSet(DbOptions.UseStoredProcs);
  29. _deleteTables = updateOptions.IsSet(DbSchemaUpdateOptions.DropUnknownTables);
  30. _supportsSchemas = driver.Supports(DbCapabilities.Schemas);
  31. _supportsClusteredIndexes = driver.Supports(DbCapabilities.ClusteredIndexes);
  32. ResetChangeInfo(_changes.OldDbModel.Tables);
  33. ResetChangeInfo(_newModel.Tables);
  34. MatchObjectsWithPeers();
  35. MarkObjectsForDrop();
  36. MarkObjectsForAdd();
  37. AnalyzeObjectsChanges();
  38. BuildChangeActions();
  39. _changes.Actions.Sort(DbSchemaUpdateAction.ByChangeType);
  40. ResetPeers(_newModel.Tables); //make sure we drop references to old model
  41. return _changes;
  42. }//class
  43. // Returns true if schema is present in New (current model); by default we do not do anything with
  44. // objects in inactive schemas
  45. private bool IsActive(string schema) {
  46. if (!_supportsSchemas && string.IsNullOrEmpty(schema))
  47. return true;
  48. return _changes.NewDbModel.ContainsSchema(schema);
  49. }
  50. private void ResetPeers(IEnumerable<DbTableInfo> tables) {
  51. foreach (var t in tables) {
  52. t.Peer = null;
  53. foreach (var c in t.Columns)
  54. c.Peer = null;
  55. foreach(var k in t.Keys)
  56. k.Peer = null;
  57. foreach(var rc in t.RefConstraints)
  58. rc.Peer = null;
  59. }
  60. }
  61. private static void ResetChangeInfo(IEnumerable<DbTableInfo> tables) {
  62. foreach (var t in tables) {
  63. t.ChangeType = DbObjectChange.None;
  64. t.Peer = null;
  65. foreach (var c in t.Columns) {
  66. c.ChangeType = DbObjectChange.None;
  67. c.Peer = null;
  68. }
  69. foreach (var key in t.Keys) {
  70. key.ChangeType = DbObjectChange.None;
  71. key.Peer = null;
  72. }
  73. foreach (var rc in t.RefConstraints) {
  74. rc.ChangeType = DbObjectChange.None;
  75. rc.Peer = null;
  76. }
  77. }
  78. }
  79. #region Matching objects - set Peer property on each object in new and old models
  80. private void MatchObjectsWithPeers() {
  81. //Go through tables in new model and try to find match in old model
  82. foreach(var newT in _newModel.Tables) {
  83. var oldT = FindOldTable(newT);
  84. if (oldT == null) {
  85. newT.ChangeType = DbObjectChange.New;
  86. continue; //next table
  87. }
  88. //We found matching table - link them; then match columns and keys in both tables
  89. oldT.Peer = newT;
  90. newT.Peer = oldT;
  91. // Match columns
  92. foreach (var newCol in newT.Columns) {
  93. var oldCol = FindOldColumn(newCol, oldT);
  94. if (oldCol == null) continue;
  95. oldCol.Peer = newCol;
  96. newCol.Peer = oldCol;
  97. }//foreach newCol
  98. //Match keys
  99. foreach (var newK in newT.Keys) {
  100. //first by name
  101. var oldK = FindOldKey(oldT, newK);
  102. if (oldK == null)
  103. continue;
  104. oldK.Peer = newK;
  105. newK.Peer = oldK;
  106. }//foreach newKey
  107. } //foreach newT
  108. // Match ref constraints - in a separate loop, after all tables and keys are matched
  109. foreach (var oldT in _oldModel.Tables) {
  110. //Detect changes - only if there's object in new model
  111. if (oldT.Peer == null) continue;
  112. foreach (var newRc in oldT.Peer.RefConstraints) {
  113. var oldRc = FindOldRefConstraint(oldT, newRc);
  114. if (oldRc == null) continue;
  115. oldRc.Peer = newRc;
  116. newRc.Peer = oldRc;
  117. } //foreach newRc
  118. }//foreach oldT
  119. //Match stored procs
  120. if (_useStoredProcs && _optons.IsSet(DbSchemaUpdateOptions.VerifyRoutines)) {
  121. foreach (var oldCmd in _oldModel.Commands) {
  122. oldCmd.Peer = _newModel.GetCommand(oldCmd.FullCommandName);
  123. if (oldCmd.Peer != null)
  124. oldCmd.Peer.Peer = oldCmd;
  125. }
  126. }//if useStoredProcs
  127. }//method
  128. // Utility methods used in matching ----------------------------------------------------------
  129. private DbTableInfo FindOldTable(DbTableInfo newTable) {
  130. DbTableInfo oldT = _oldModel.GetTable(newTable.FullName);
  131. if (oldT != null) return oldT;
  132. var entity = newTable.Entity;
  133. if(entity == null) return null; //just in case, if we ever have tables without entities
  134. if (entity.OldNames == null) return null;
  135. foreach (var oldName in entity.OldNames) {
  136. oldT = _oldModel.GetTable(newTable.SchemaName, oldName);
  137. if (oldT != null) return oldT;
  138. //if old name starts with "I" (like all interfaces), then try without I
  139. if (oldName.StartsWith("I"))
  140. oldT = _oldModel.GetTable(newTable.SchemaName, oldName.Substring(1));
  141. if (oldT != null) return oldT;
  142. }//foreach oldName
  143. return null;
  144. }
  145. private DbColumnInfo FindOldColumn(DbColumnInfo newCol, DbTableInfo oldTable) {
  146. var oldCol = oldTable.Columns.FirstOrDefault(c => c.ColumnName == newCol.ColumnName);
  147. if (oldCol != null) return oldCol;
  148. // Try finding by old name
  149. var oldNames = newCol.Member.OldNames;
  150. if (oldNames == null) return null;
  151. foreach (var oldName in oldNames) {
  152. oldCol = oldTable.Columns.FirstOrDefault(c => c.ColumnName == oldName);
  153. if (oldCol != null)
  154. return oldCol;
  155. }
  156. return null;
  157. }
  158. private DbKeyInfo FindOldKey(DbTableInfo oldTable, DbKeyInfo newKey) {
  159. // just for easier debugging, preselect keys matching by type and column count
  160. var similarOldKeys = oldTable.Keys.Where(k => k.KeyType == newKey.KeyType && k.Columns.Count == newKey.Columns.Count).ToList();
  161. foreach (var oldKey in similarOldKeys) {
  162. // If we have duplicating keys (ex: Northwind database, OrdersTable, key CustomerID, CustomerOrders), then lets try match them in pairs, to accurately report differences
  163. if (oldKey.Peer != null)
  164. continue;
  165. if (DbKeysMatch(oldKey, newKey))
  166. return oldKey;
  167. }
  168. return null;
  169. }
  170. private bool DbKeysMatch(DbKeyInfo oldKey, DbKeyInfo newKey) {
  171. if(oldKey.KeyType != newKey.KeyType ||
  172. oldKey.Columns.Count != newKey.Columns.Count) return false;
  173. //check column-by-column match
  174. for(int i = 0; i < oldKey.Columns.Count; i++) {
  175. var oldKeyCol = oldKey.Columns[i];
  176. var newKeyCol = newKey.Columns[i];
  177. if (oldKeyCol.Peer != newKeyCol)
  178. return false;
  179. var oldIsDesc = oldKey.DescendingColumns.Contains(oldKeyCol);
  180. var newIsDesc = newKey.DescendingColumns.Contains(newKeyCol);
  181. if (oldIsDesc != newIsDesc)
  182. return false;
  183. }
  184. return true;
  185. }
  186. private DbRefConstraintInfo FindOldRefConstraint(DbTableInfo oldTable, DbRefConstraintInfo newRefConstraint) {
  187. var newFrom = newRefConstraint.FromKey;
  188. var newTo = newRefConstraint.ToKey;
  189. foreach (var oldRc in oldTable.RefConstraints)
  190. if (oldRc.FromKey.Peer == newFrom && oldRc.ToKey.Peer == newTo)
  191. return oldRc;
  192. return null;
  193. }
  194. #endregion
  195. #region Analyzing changes
  196. // Sets objects with no peers in old model to "DROP" change type; objects in new model with no peers are set to "New"
  197. private void MarkObjectsForDrop() {
  198. foreach (var T in _oldModel.Tables) {
  199. if (!IsActive(T.SchemaName)) continue;
  200. if (T.Peer == null && _deleteTables) {
  201. T.ChangeType = DbObjectChange.Drop;
  202. }
  203. foreach (var c in T.Columns)
  204. if (c.Peer == null) c.ChangeType = DbObjectChange.Drop;
  205. //do not drop not supported 'strange' keys, let them stay; also, key with no columns is fake PK that VITA creates if there is no PK on the table
  206. foreach (var k in T.Keys) {
  207. if (T.ChangeType == DbObjectChange.Drop)
  208. continue; // if table is dropped, all keys will be dropped with it
  209. if (k.Peer == null && k.Columns.Count > 0 && !k.NotSupported)
  210. k.ChangeType = DbObjectChange.Drop;
  211. }
  212. foreach (var rc in T.RefConstraints)
  213. if (rc.Peer == null)
  214. rc.ChangeType = DbObjectChange.Drop;
  215. }//tables
  216. }
  217. private void MarkObjectsForAdd() {
  218. foreach (var T in _newModel.Tables) {
  219. if (T.Peer == null) T.ChangeType = DbObjectChange.New;
  220. foreach (var c in T.Columns)
  221. if (c.Peer == null) c.ChangeType = DbObjectChange.New;
  222. foreach (var k in T.Keys)
  223. if (k.Peer == null && k.Columns.Count > 0) // when there is no PK on the table, VITA creates fake PK with no columns - do not create this in database
  224. k.ChangeType = DbObjectChange.New;
  225. foreach (var rc in T.RefConstraints)
  226. if (rc.Peer == null) rc.ChangeType = DbObjectChange.New;
  227. }
  228. }
  229. //Go only through those objects that have peers
  230. private void AnalyzeObjectsChanges() {
  231. //Go through old tables and analyze changes
  232. foreach (var oldT in _oldModel.Tables) {
  233. if (oldT.Peer == null) continue;
  234. if (!IsActive(oldT.SchemaName)) continue;
  235. //Check columns changes
  236. bool anyChanged = false;
  237. foreach (var oldCol in oldT.Columns)
  238. anyChanged |= CheckColumnChanged(oldCol);
  239. //Check if table columns changed; if yes, mark old table as modified
  240. if (oldT.ChangeType == DbObjectChange.None && (anyChanged || oldT.Peer.Columns.Count > oldT.Columns.Count))
  241. oldT.ChangeType = DbObjectChange.Modify;
  242. //check all keys - if any column in the key changed, mark key as changed
  243. foreach (var key in oldT.Keys) {
  244. anyChanged = key.Columns.Any(c => c.ChangeType != DbObjectChange.None);
  245. if (key.ChangeType == DbObjectChange.None && anyChanged)
  246. key.ChangeType = DbObjectChange.Modify;
  247. }
  248. }//foreach oldT
  249. // Now check ref constraints - if From or To key changed, mark constraint as modified.
  250. // We must do it in a separate loop, after setting change status for all keys in all tables
  251. foreach (var oldT in _oldModel.Tables) {
  252. if (oldT.Peer == null) continue;
  253. foreach (var refc in oldT.RefConstraints) {
  254. if (refc.ChangeType != DbObjectChange.None) continue;
  255. if (refc.ToKey.Table.ChangeType == DbObjectChange.Drop) {
  256. refc.ChangeType = DbObjectChange.Drop;
  257. continue;
  258. }
  259. var modified = refc.Peer == null || refc.CascadeDelete != refc.Peer.CascadeDelete ||
  260. refc.FromKey.ChangeType != DbObjectChange.None || refc.ToKey.ChangeType != DbObjectChange.None;
  261. if (modified)
  262. refc.ChangeType = DbObjectChange.Modify;
  263. }//foreach refc
  264. }//foreach oldT
  265. }//mehtod
  266. private bool CheckColumnChanged(DbColumnInfo oldColumn) {
  267. //Note: we check renaming separately when we build change list
  268. if (oldColumn.Peer == null)
  269. oldColumn.ChangeType = DbObjectChange.Drop;
  270. else if (!ColumnsMatch(oldColumn, oldColumn.Peer))
  271. oldColumn.ChangeType = DbObjectChange.Modify;
  272. return oldColumn.ChangeType != DbObjectChange.None;
  273. }//method
  274. private static bool ColumnsMatch(DbColumnInfo x, DbColumnInfo y) {
  275. var nullableMatch = x.Flags.IsSet(DbColumnFlags.Nullable) == y.Flags.IsSet(DbColumnFlags.Nullable);
  276. var identityMatch = x.Flags.IsSet(DbColumnFlags.Identity) == y.Flags.IsSet(DbColumnFlags.Identity);
  277. var dbTypesMatch = x.TypeInfo.DbType == y.TypeInfo.DbType;
  278. var scalePrecMatch = x.TypeInfo.ClrType == typeof(decimal) ? x.TypeInfo.Scale == y.TypeInfo.Scale && x.TypeInfo.Precision == y.TypeInfo.Precision : true;
  279. var clrType = x.TypeInfo.ClrType;
  280. var sizeMatters = clrType == typeof(string) || clrType == typeof(byte[]);
  281. var sizeMatch = sizeMatters ? x.TypeInfo.Size == y.TypeInfo.Size : true;
  282. return (x.TypeInfo.DbType == y.TypeInfo.DbType && nullableMatch && identityMatch && sizeMatch && dbTypesMatch && scalePrecMatch);
  283. // && x.TypeInfo.DefaultExpression == y.TypeInfo.DefaultExpression);
  284. //NOTE: default expresssion cannot be changed after column is created (MSSQL).
  285. }
  286. #endregion
  287. #region Building change actions
  288. private void BuildChangeActions() {
  289. BuildRenameActions();
  290. BuildModifyDropActions();
  291. BuildAddActions();
  292. if (_optons.IsSet(DbSchemaUpdateOptions.VerifyRoutines))
  293. BuildStoredProcActions();
  294. }
  295. private void BuildModifyDropActions() {
  296. // Objects in OLD model: drop or modify
  297. foreach (var oldT in _oldModel.Tables) {
  298. switch(oldT.ChangeType) {
  299. case DbObjectChange.Drop: AddChangeAction(DbSchemaChangeActionType.TableDrop, oldT); break;
  300. case DbObjectChange.Modify:
  301. //Check columns
  302. foreach (var oldCol in oldT.Columns) {
  303. if (oldCol.Flags.IsSet(DbColumnFlags.Error)) continue; //do not do anything about column in error
  304. switch (oldCol.ChangeType) {
  305. case DbObjectChange.Drop: AddChangeAction(DbSchemaChangeActionType.ColumnDrop, oldCol); break;
  306. case DbObjectChange.Modify:
  307. var newCol = oldCol.Peer;
  308. bool needsInit = oldCol.Flags.IsSet(DbColumnFlags.Nullable) &&
  309. !newCol.Flags.IsSet(DbColumnFlags.Nullable) && newCol.DefaultExpression == null;
  310. if (needsInit)
  311. AddChangeAction(DbSchemaChangeActionType.ColumnInit, newCol);
  312. AddChangeAction(DbSchemaChangeActionType.ColumnModify, newCol);
  313. break;
  314. }//switch
  315. }//foreach oldCol
  316. break;
  317. }//switch
  318. // Keys
  319. foreach (var key in oldT.Keys) {
  320. if (key.KeyType.IsSet(KeyType.Auto) || key.ChangeType == DbObjectChange.None) continue;
  321. AddChangeAction(DbSchemaChangeActionType.KeyDrop, key);
  322. if (key.Peer != null)
  323. AddKeyAddAction(key.Peer);
  324. }
  325. //Ref constraints
  326. foreach (var refc in oldT.RefConstraints)
  327. if (refc.ChangeType != DbObjectChange.None) {
  328. AddChangeAction(DbSchemaChangeActionType.RefConstraintDrop, refc);
  329. if (refc.Peer != null && _useRefIntegrity)
  330. AddChangeAction(DbSchemaChangeActionType.RefConstraintAdd, refc.Peer);
  331. }
  332. }//foreach oldT
  333. }
  334. private void BuildAddActions() {
  335. // Schemas
  336. if (_supportsSchemas) {
  337. foreach (var schema in _newModel.Schemas)
  338. if (!_oldModel.ContainsSchema(schema))
  339. AddChangeAction(DbSchemaChangeActionType.SchemaAdd, schema);
  340. }
  341. //Tables and columns
  342. foreach (var newT in _newModel.Tables) {
  343. switch (newT.ChangeType) {
  344. case DbObjectChange.New:
  345. AddChangeAction(DbSchemaChangeActionType.TableAdd, newT);
  346. break;
  347. default:
  348. var changing = (newT.Peer != null && newT.Peer.ChangeType != DbObjectChange.None);
  349. if (!changing) break;
  350. foreach (var col in newT.Columns)
  351. if(col.ChangeType == DbObjectChange.New) {
  352. AddChangeAction(DbSchemaChangeActionType.ColumnAdd, col);
  353. bool needsInit = !col.Flags.IsSet(DbColumnFlags.Nullable) && col.DefaultExpression == null;
  354. if (needsInit) {
  355. AddChangeAction(DbSchemaChangeActionType.ColumnAddedInit, col);
  356. AddChangeAction(DbSchemaChangeActionType.ColumnAddComplete, col);
  357. }
  358. }//foreach col
  359. break;
  360. }//switch
  361. //Keys and constraints
  362. foreach (var key in newT.Keys) {
  363. if (key.KeyType.IsSet(KeyType.Auto))
  364. continue;
  365. if (key.ChangeType == DbObjectChange.New)
  366. AddKeyAddAction(key);
  367. }
  368. if (_useRefIntegrity)
  369. foreach (var refc in newT.RefConstraints)
  370. if (refc.ChangeType == DbObjectChange.New)
  371. AddChangeAction(DbSchemaChangeActionType.RefConstraintAdd, refc);
  372. }//foreach newT
  373. }//method
  374. private void BuildStoredProcActions() {
  375. if (!_useStoredProcs)
  376. return;
  377. var driver = _newModel.Driver;
  378. //Delete old routines in active schema with no match
  379. foreach (var oldCmd in _oldModel.Commands) {
  380. if (oldCmd.Flags.IsSet(DbCommandFlags.NotGenerated)) // do not drop SPs that were not generated by VITA
  381. continue;
  382. if (IsActive(oldCmd.Schema) && oldCmd.Peer == null)
  383. AddChangeAction(DbSchemaChangeActionType.RoutineDrop, oldCmd.FullCommandName);
  384. }
  385. foreach(var newCmd in _newModel.Commands) {
  386. if (newCmd.Peer != null && driver.StoredProceduresMatch(newCmd, newCmd.Peer))
  387. continue;
  388. //no match
  389. if (newCmd.Peer != null)
  390. AddChangeAction(DbSchemaChangeActionType.RoutineDrop, newCmd.FullCommandName);
  391. AddChangeAction(DbSchemaChangeActionType.RoutineAdd, newCmd);
  392. }
  393. }
  394. //Note that name change is not reflected in ChangeStatus - it would be still None if only Name changed, but nothing else did not.
  395. private void BuildRenameActions() {
  396. bool tableNamesIgnoreCase = _newModel.Settings.Options.IsSet(DbOptions.IgnoreTableNamesCase);
  397. foreach (var oldT in _oldModel.Tables) {
  398. if (oldT.Peer == null) continue;
  399. if (string.Compare(oldT.TableName, oldT.Peer.TableName, tableNamesIgnoreCase) != 0)
  400. AddChangeAction(DbSchemaChangeActionType.TableRename, oldT);
  401. foreach (var c in oldT.Columns)
  402. if (c.Peer != null && c.ColumnName != c.Peer.ColumnName)
  403. AddChangeAction(DbSchemaChangeActionType.ColumnRename, c);
  404. }
  405. }
  406. private DbSchemaUpdateAction AddKeyAddAction(DbKeyInfo key) {
  407. var actionType = key.KeyType == KeyType.PrimaryKey ? DbSchemaChangeActionType.PrimaryKeyAdd : DbSchemaChangeActionType.KeyAdd;
  408. return AddChangeAction(actionType, key);
  409. }
  410. private DbSchemaUpdateAction AddChangeAction(DbSchemaChangeActionType actionType, object objectInfo) {
  411. var action = new DbSchemaUpdateAction(actionType, objectInfo);
  412. _changes.Actions.Add(action);
  413. return action;
  414. }
  415. #endregion
  416. } //class
  417. }//namespace