diff options
Diffstat (limited to '')
-rw-r--r-- | OpenSim/Data/Migration.cs | 341 |
1 files changed, 238 insertions, 103 deletions
diff --git a/OpenSim/Data/Migration.cs b/OpenSim/Data/Migration.cs index 06defe4..4f113a2 100644 --- a/OpenSim/Data/Migration.cs +++ b/OpenSim/Data/Migration.cs | |||
@@ -72,59 +72,116 @@ namespace OpenSim.Data | |||
72 | { | 72 | { |
73 | private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | 73 | private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
74 | 74 | ||
75 | private string _type; | 75 | protected string _type; |
76 | private DbConnection _conn; | 76 | protected DbConnection _conn; |
77 | // private string _subtype; | 77 | protected Assembly _assem; |
78 | private Assembly _assem; | ||
79 | private Regex _match; | ||
80 | 78 | ||
81 | private static readonly string _migrations_create = "create table migrations(name varchar(100), version int)"; | 79 | private Regex _match_old; |
82 | // private static readonly string _migrations_init = "insert into migrations values('migrations', 1)"; | 80 | private Regex _match_new; |
83 | // private static readonly string _migrations_find = "select version from migrations where name='migrations'"; | ||
84 | 81 | ||
82 | /// <summary>Have the parameterless constructor just so we can specify it as a generic parameter with the new() constraint. | ||
83 | /// Currently this is only used in the tests. A Migration instance created this way must be then | ||
84 | /// initialized with Initialize(). Regular creation should be through the parameterized constructors. | ||
85 | /// </summary> | ||
86 | public Migration() | ||
87 | { | ||
88 | } | ||
85 | 89 | ||
86 | public Migration(DbConnection conn, Assembly assem, string type) | 90 | public Migration(DbConnection conn, Assembly assem, string subtype, string type) |
87 | { | 91 | { |
88 | _type = type; | 92 | Initialize(conn, assem, type, subtype); |
89 | _conn = conn; | ||
90 | _assem = assem; | ||
91 | _match = new Regex(@"\.(\d\d\d)_" + _type + @"\.sql"); | ||
92 | Initialize(); | ||
93 | } | 93 | } |
94 | 94 | ||
95 | public Migration(DbConnection conn, Assembly assem, string subtype, string type) | 95 | public Migration(DbConnection conn, Assembly assem, string type) |
96 | { | ||
97 | Initialize(conn, assem, type, ""); | ||
98 | } | ||
99 | |||
100 | /// <summary>Must be called after creating with the parameterless constructor. | ||
101 | /// NOTE that the Migration class now doesn't access database in any way during initialization. | ||
102 | /// Specifically, it won't check if the [migrations] table exists. Such checks are done later: | ||
103 | /// automatically on Update(), or you can explicitly call InitMigrationsTable(). | ||
104 | /// </summary> | ||
105 | /// <param name="conn"></param> | ||
106 | /// <param name="assem"></param> | ||
107 | /// <param name="subtype"></param> | ||
108 | /// <param name="type"></param> | ||
109 | public void Initialize (DbConnection conn, Assembly assem, string type, string subtype) | ||
96 | { | 110 | { |
97 | _type = type; | 111 | _type = type; |
98 | _conn = conn; | 112 | _conn = conn; |
99 | _assem = assem; | 113 | _assem = assem; |
100 | _match = new Regex(subtype + @"\.(\d\d\d)_" + _type + @"\.sql"); | 114 | _match_old = new Regex(subtype + @"\.(\d\d\d)_" + _type + @"\.sql"); |
101 | Initialize(); | 115 | string s = String.IsNullOrEmpty(subtype) ? _type : _type + @"\." + subtype; |
116 | _match_new = new Regex(@"\." + s + @"\.migrations(?:\.(?<ver>\d+)$|.*)"); | ||
102 | } | 117 | } |
103 | 118 | ||
104 | private void Initialize() | 119 | public void InitMigrationsTable() |
105 | { | 120 | { |
106 | // clever, eh, we figure out which migrations version we are | 121 | // NOTE: normally when the [migrations] table is created, the version record for 'migrations' is |
107 | int migration_version = FindVersion(_conn, "migrations"); | 122 | // added immediately. However, if for some reason the table is there but empty, we want to handle that as well. |
108 | 123 | int ver = FindVersion(_conn, "migrations"); | |
109 | if (migration_version > 0) | 124 | if (ver <= 0) // -1 = no table, 0 = no version record |
110 | return; | 125 | { |
126 | if( ver < 0 ) | ||
127 | ExecuteScript("create table migrations(name varchar(100), version int)"); | ||
128 | InsertVersion("migrations", 1); | ||
129 | } | ||
130 | } | ||
111 | 131 | ||
112 | // If not, create the migration tables | 132 | /// <summary>Executes a script, possibly in a database-specific way. |
113 | using (DbCommand cmd = _conn.CreateCommand()) | 133 | /// It can be redefined for a specific DBMS, if necessary. Specifically, |
134 | /// to avoid problems with proc definitions in MySQL, we must use | ||
135 | /// MySqlScript class instead of just DbCommand. We don't want to bring | ||
136 | /// MySQL references here, so instead define a MySQLMigration class | ||
137 | /// in OpenSim.Data.MySQL | ||
138 | /// </summary> | ||
139 | /// <param name="conn"></param> | ||
140 | /// <param name="script">Array of strings, one-per-batch (often just one)</param> | ||
141 | protected virtual void ExecuteScript(DbConnection conn, string[] script) | ||
142 | { | ||
143 | using (DbCommand cmd = conn.CreateCommand()) | ||
114 | { | 144 | { |
115 | cmd.CommandText = _migrations_create; | 145 | cmd.CommandTimeout = 0; |
116 | cmd.ExecuteNonQuery(); | 146 | foreach (string sql in script) |
147 | { | ||
148 | cmd.CommandText = sql; | ||
149 | try | ||
150 | { | ||
151 | cmd.ExecuteNonQuery(); | ||
152 | } | ||
153 | catch(Exception e) | ||
154 | { | ||
155 | throw new Exception(e.Message + " in SQL: " + sql); | ||
156 | } | ||
157 | } | ||
117 | } | 158 | } |
159 | } | ||
118 | 160 | ||
119 | InsertVersion("migrations", 1); | 161 | protected void ExecuteScript(DbConnection conn, string sql) |
162 | { | ||
163 | ExecuteScript(conn, new string[]{sql}); | ||
164 | } | ||
165 | |||
166 | protected void ExecuteScript(string sql) | ||
167 | { | ||
168 | ExecuteScript(_conn, sql); | ||
169 | } | ||
170 | |||
171 | protected void ExecuteScript(string[] script) | ||
172 | { | ||
173 | ExecuteScript(_conn, script); | ||
120 | } | 174 | } |
121 | 175 | ||
176 | |||
177 | |||
122 | public void Update() | 178 | public void Update() |
123 | { | 179 | { |
124 | int version = 0; | 180 | InitMigrationsTable(); |
125 | version = FindVersion(_conn, _type); | ||
126 | 181 | ||
127 | SortedList<int, string> migrations = GetMigrationsAfter(version); | 182 | int version = FindVersion(_conn, _type); |
183 | |||
184 | SortedList<int, string[]> migrations = GetMigrationsAfter(version); | ||
128 | if (migrations.Count < 1) | 185 | if (migrations.Count < 1) |
129 | return; | 186 | return; |
130 | 187 | ||
@@ -132,57 +189,40 @@ namespace OpenSim.Data | |||
132 | m_log.InfoFormat("[MIGRATIONS] Upgrading {0} to latest revision {1}.", _type, migrations.Keys[migrations.Count - 1]); | 189 | m_log.InfoFormat("[MIGRATIONS] Upgrading {0} to latest revision {1}.", _type, migrations.Keys[migrations.Count - 1]); |
133 | m_log.Info("[MIGRATIONS] NOTE: this may take a while, don't interupt this process!"); | 190 | m_log.Info("[MIGRATIONS] NOTE: this may take a while, don't interupt this process!"); |
134 | 191 | ||
135 | using (DbCommand cmd = _conn.CreateCommand()) | 192 | foreach (KeyValuePair<int, string[]> kvp in migrations) |
136 | { | 193 | { |
137 | foreach (KeyValuePair<int, string> kvp in migrations) | 194 | int newversion = kvp.Key; |
195 | // we need to up the command timeout to infinite as we might be doing long migrations. | ||
196 | |||
197 | /* [AlexRa 01-May-10]: We can't always just run any SQL in a single batch (= ExecuteNonQuery()). Things like | ||
198 | * stored proc definitions might have to be sent to the server each in a separate batch. | ||
199 | * This is certainly so for MS SQL; not sure how the MySQL connector sorts out the mess | ||
200 | * with 'delimiter @@'/'delimiter ;' around procs. So each "script" this code executes now is not | ||
201 | * a single string, but an array of strings, executed separately. | ||
202 | */ | ||
203 | try | ||
138 | { | 204 | { |
139 | int newversion = kvp.Key; | 205 | ExecuteScript(kvp.Value); |
140 | cmd.CommandText = kvp.Value; | 206 | } |
141 | // we need to up the command timeout to infinite as we might be doing long migrations. | 207 | catch (Exception e) |
142 | cmd.CommandTimeout = 0; | 208 | { |
143 | try | 209 | m_log.DebugFormat("[MIGRATIONS] Cmd was {0}", e.Message.Replace("\n", " ")); |
144 | { | 210 | m_log.Debug("[MIGRATIONS]: An error has occurred in the migration. This may mean you could see errors trying to run OpenSim. If you see database related errors, you will need to fix the issue manually. Continuing."); |
145 | cmd.ExecuteNonQuery(); | 211 | ExecuteScript("ROLLBACK;"); |
146 | } | 212 | } |
147 | catch (Exception e) | ||
148 | { | ||
149 | m_log.DebugFormat("[MIGRATIONS] Cmd was {0}", cmd.CommandText); | ||
150 | m_log.DebugFormat("[MIGRATIONS]: An error has occurred in the migration {0}.\n This may mean you could see errors trying to run OpenSim. If you see database related errors, you will need to fix the issue manually. Continuing.", e.Message); | ||
151 | cmd.CommandText = "ROLLBACK;"; | ||
152 | cmd.ExecuteNonQuery(); | ||
153 | } | ||
154 | 213 | ||
155 | if (version == 0) | 214 | if (version == 0) |
156 | { | 215 | { |
157 | InsertVersion(_type, newversion); | 216 | InsertVersion(_type, newversion); |
158 | } | ||
159 | else | ||
160 | { | ||
161 | UpdateVersion(_type, newversion); | ||
162 | } | ||
163 | version = newversion; | ||
164 | } | 217 | } |
218 | else | ||
219 | { | ||
220 | UpdateVersion(_type, newversion); | ||
221 | } | ||
222 | version = newversion; | ||
165 | } | 223 | } |
166 | } | 224 | } |
167 | 225 | ||
168 | // private int MaxVersion() | ||
169 | // { | ||
170 | // int max = 0; | ||
171 | // string[] names = _assem.GetManifestResourceNames(); | ||
172 | |||
173 | // foreach (string s in names) | ||
174 | // { | ||
175 | // Match m = _match.Match(s); | ||
176 | // if (m.Success) | ||
177 | // { | ||
178 | // int MigrationVersion = int.Parse(m.Groups[1].ToString()); | ||
179 | // if (MigrationVersion > max) | ||
180 | // max = MigrationVersion; | ||
181 | // } | ||
182 | // } | ||
183 | // return max; | ||
184 | // } | ||
185 | |||
186 | public int Version | 226 | public int Version |
187 | { | 227 | { |
188 | get { return FindVersion(_conn, _type); } | 228 | get { return FindVersion(_conn, _type); } |
@@ -206,7 +246,7 @@ namespace OpenSim.Data | |||
206 | try | 246 | try |
207 | { | 247 | { |
208 | cmd.CommandText = "select version from migrations where name='" + type + "' order by version desc"; | 248 | cmd.CommandText = "select version from migrations where name='" + type + "' order by version desc"; |
209 | using (IDataReader reader = cmd.ExecuteReader()) | 249 | using (DbDataReader reader = cmd.ExecuteReader()) |
210 | { | 250 | { |
211 | if (reader.Read()) | 251 | if (reader.Read()) |
212 | { | 252 | { |
@@ -217,7 +257,8 @@ namespace OpenSim.Data | |||
217 | } | 257 | } |
218 | catch | 258 | catch |
219 | { | 259 | { |
220 | // Something went wrong, so we're version 0 | 260 | // Something went wrong (probably no table), so we're at version -1 |
261 | version = -1; | ||
221 | } | 262 | } |
222 | } | 263 | } |
223 | return version; | 264 | return version; |
@@ -225,57 +266,151 @@ namespace OpenSim.Data | |||
225 | 266 | ||
226 | private void InsertVersion(string type, int version) | 267 | private void InsertVersion(string type, int version) |
227 | { | 268 | { |
228 | using (DbCommand cmd = _conn.CreateCommand()) | 269 | m_log.InfoFormat("[MIGRATIONS]: Creating {0} at version {1}", type, version); |
229 | { | 270 | ExecuteScript("insert into migrations(name, version) values('" + type + "', " + version + ")"); |
230 | cmd.CommandText = "insert into migrations(name, version) values('" + type + "', " + version + ")"; | ||
231 | m_log.InfoFormat("[MIGRATIONS]: Creating {0} at version {1}", type, version); | ||
232 | cmd.ExecuteNonQuery(); | ||
233 | } | ||
234 | } | 271 | } |
235 | 272 | ||
236 | private void UpdateVersion(string type, int version) | 273 | private void UpdateVersion(string type, int version) |
237 | { | 274 | { |
238 | using (DbCommand cmd = _conn.CreateCommand()) | 275 | m_log.InfoFormat("[MIGRATIONS]: Updating {0} to version {1}", type, version); |
239 | { | 276 | ExecuteScript("update migrations set version=" + version + " where name='" + type + "'"); |
240 | cmd.CommandText = "update migrations set version=" + version + " where name='" + type + "'"; | ||
241 | m_log.InfoFormat("[MIGRATIONS]: Updating {0} to version {1}", type, version); | ||
242 | cmd.ExecuteNonQuery(); | ||
243 | } | ||
244 | } | 277 | } |
245 | 278 | ||
246 | // private SortedList<int, string> GetAllMigrations() | 279 | private delegate void FlushProc(); |
247 | // { | ||
248 | // return GetMigrationsAfter(0); | ||
249 | // } | ||
250 | 280 | ||
251 | private SortedList<int, string> GetMigrationsAfter(int after) | 281 | /// <summary>Scans for migration resources in either old-style "scattered" (one file per version) |
282 | /// or new-style "integrated" format (single file with ":VERSION nnn" sections). | ||
283 | /// In the new-style migrations it also recognizes ':GO' separators for parts of the SQL script | ||
284 | /// that must be sent to the server separately. The old-style migrations are loaded each in one piece | ||
285 | /// and don't support the ':GO' feature. | ||
286 | /// </summary> | ||
287 | /// <param name="after">The version we are currently at. Scan for any higher versions</param> | ||
288 | /// <returns>A list of string arrays, representing the scripts.</returns> | ||
289 | private SortedList<int, string[]> GetMigrationsAfter(int after) | ||
252 | { | 290 | { |
291 | SortedList<int, string[]> migrations = new SortedList<int, string[]>(); | ||
292 | |||
253 | string[] names = _assem.GetManifestResourceNames(); | 293 | string[] names = _assem.GetManifestResourceNames(); |
254 | SortedList<int, string> migrations = new SortedList<int, string>(); | 294 | if( names.Length == 0 ) // should never happen |
255 | // because life is funny if we don't | 295 | return migrations; |
256 | Array.Sort(names); | 296 | |
297 | Array.Sort(names); // we want all the migrations ordered | ||
298 | |||
299 | int nLastVerFound = 0; | ||
300 | Match m = null; | ||
301 | string sFile = Array.FindLast(names, nm => { m = _match_new.Match(nm); return m.Success; }); // ; nm.StartsWith(sPrefix, StringComparison.InvariantCultureIgnoreCase | ||
302 | |||
303 | if( (m != null) && !String.IsNullOrEmpty(sFile) ) | ||
304 | { | ||
305 | /* The filename should be '<StoreName>.migrations[.NNN]' where NNN | ||
306 | * is the last version number defined in the file. If the '.NNN' part is recognized, the code can skip | ||
307 | * the file without looking inside if we have a higher version already. Without the suffix we read | ||
308 | * the file anyway and use the version numbers inside. Any unrecognized suffix (such as '.sql') | ||
309 | * is valid but ignored. | ||
310 | * | ||
311 | * NOTE that we expect only one 'merged' migration file. If there are several, we take the last one. | ||
312 | * If you are numbering them, leave only the latest one in the project or at least make sure they numbered | ||
313 | * to come up in the correct order (e.g. 'SomeStore.migrations.001' rather than 'SomeStore.migrations.1') | ||
314 | */ | ||
315 | |||
316 | if (m.Groups.Count > 1 && int.TryParse(m.Groups[1].Value, out nLastVerFound)) | ||
317 | { | ||
318 | if( nLastVerFound <= after ) | ||
319 | goto scan_old_style; | ||
320 | } | ||
321 | |||
322 | System.Text.StringBuilder sb = new System.Text.StringBuilder(4096); | ||
323 | int nVersion = -1; | ||
324 | |||
325 | List<string> script = new List<string>(); | ||
326 | |||
327 | FlushProc flush = delegate() | ||
328 | { | ||
329 | if (sb.Length > 0) // last SQL stmt to script list | ||
330 | { | ||
331 | script.Add(sb.ToString()); | ||
332 | sb.Length = 0; | ||
333 | } | ||
257 | 334 | ||
335 | if ( (nVersion > 0) && (nVersion > after) && (script.Count > 0) && !migrations.ContainsKey(nVersion)) // script to the versioned script list | ||
336 | { | ||
337 | migrations[nVersion] = script.ToArray(); | ||
338 | } | ||
339 | script.Clear(); | ||
340 | }; | ||
341 | |||
342 | using (Stream resource = _assem.GetManifestResourceStream(sFile)) | ||
343 | using (StreamReader resourceReader = new StreamReader(resource)) | ||
344 | { | ||
345 | int nLineNo = 0; | ||
346 | while (!resourceReader.EndOfStream) | ||
347 | { | ||
348 | string sLine = resourceReader.ReadLine(); | ||
349 | nLineNo++; | ||
350 | |||
351 | if( String.IsNullOrEmpty(sLine) || sLine.StartsWith("#") ) // ignore a comment or empty line | ||
352 | continue; | ||
353 | |||
354 | if (sLine.Trim().Equals(":GO", StringComparison.InvariantCultureIgnoreCase)) | ||
355 | { | ||
356 | if (sb.Length == 0) continue; | ||
357 | if (nVersion > after) | ||
358 | script.Add(sb.ToString()); | ||
359 | sb.Length = 0; | ||
360 | continue; | ||
361 | } | ||
362 | |||
363 | if (sLine.StartsWith(":VERSION ", StringComparison.InvariantCultureIgnoreCase)) // ":VERSION nnn" | ||
364 | { | ||
365 | flush(); | ||
366 | |||
367 | int n = sLine.IndexOf('#'); // Comment is allowed in version sections, ignored | ||
368 | if (n >= 0) | ||
369 | sLine = sLine.Substring(0, n); | ||
370 | |||
371 | if (!int.TryParse(sLine.Substring(9).Trim(), out nVersion)) | ||
372 | { | ||
373 | m_log.ErrorFormat("[MIGRATIONS]: invalid version marker at {0}: line {1}. Migration failed!", sFile, nLineNo); | ||
374 | break; | ||
375 | } | ||
376 | } | ||
377 | else | ||
378 | { | ||
379 | sb.AppendLine(sLine); | ||
380 | } | ||
381 | } | ||
382 | flush(); | ||
383 | |||
384 | // If there are scattered migration files as well, only look for those with higher version numbers. | ||
385 | if (after < nVersion) | ||
386 | after = nVersion; | ||
387 | } | ||
388 | } | ||
389 | |||
390 | scan_old_style: | ||
391 | // scan "old style" migration pieces anyway, ignore any versions already filled from the single file | ||
258 | foreach (string s in names) | 392 | foreach (string s in names) |
259 | { | 393 | { |
260 | Match m = _match.Match(s); | 394 | m = _match_old.Match(s); |
261 | if (m.Success) | 395 | if (m.Success) |
262 | { | 396 | { |
263 | int version = int.Parse(m.Groups[1].ToString()); | 397 | int version = int.Parse(m.Groups[1].ToString()); |
264 | if (version > after) | 398 | if ( (version > after) && !migrations.ContainsKey(version) ) |
265 | { | 399 | { |
266 | using (Stream resource = _assem.GetManifestResourceStream(s)) | 400 | using (Stream resource = _assem.GetManifestResourceStream(s)) |
267 | { | 401 | { |
268 | using (StreamReader resourceReader = new StreamReader(resource)) | 402 | using (StreamReader resourceReader = new StreamReader(resource)) |
269 | { | 403 | { |
270 | string resourceString = resourceReader.ReadToEnd(); | 404 | string sql = resourceReader.ReadToEnd(); |
271 | migrations.Add(version, resourceString); | 405 | migrations.Add(version, new string[]{sql}); |
272 | } | 406 | } |
273 | } | 407 | } |
274 | } | 408 | } |
275 | } | 409 | } |
276 | } | 410 | } |
277 | 411 | ||
278 | if (migrations.Count < 1) { | 412 | if (migrations.Count < 1) |
413 | { | ||
279 | m_log.InfoFormat("[MIGRATIONS]: {0} up to date, no migrations to apply", _type); | 414 | m_log.InfoFormat("[MIGRATIONS]: {0} up to date, no migrations to apply", _type); |
280 | } | 415 | } |
281 | return migrations; | 416 | return migrations; |