aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Data/Migration.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Data/Migration.cs')
-rw-r--r--OpenSim/Data/Migration.cs341
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
390scan_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;