versaplexd: "Re-quote" the parameter to LIST COLUMNS.
[versaplex.git] / versaplexd / vxschema.cs
blob386193e440f43d0a852de91ace2170bc3a6a1a4d
1 using System;
2 using System.IO;
3 using System.Text;
4 using System.Collections;
5 using System.Collections.Generic;
6 using System.Security.Cryptography;
7 using Wv;
8 using Wv.Extensions;
10 [Flags]
11 public enum VxCopyOpts : int
13 None = 0,
14 DryRun = 0x1,
15 ShowProgress = 0x2,
16 ShowDiff = 0x4,
17 Destructive = 0x8,
19 Verbose = ShowProgress | ShowDiff,
22 internal class VxSchemaElement : IComparable
24 string _type;
25 public string type {
26 get { return _type; }
29 string _name;
30 public string name {
31 get { return _name; }
34 string _text;
35 public virtual string text {
36 get { return _text; }
37 set { _text = value;}
40 bool _encrypted;
41 public bool encrypted {
42 get { return _encrypted; }
45 public string key {
46 get { return type + "/" + name; }
49 public static VxSchemaElement create(string type, string name,
50 string text, bool encrypted)
52 try {
53 if (type == "Table")
54 return new VxSchemaTable(name, text);
55 } catch (ArgumentException) {
56 // if the table data is invalid, just ignore it.
57 // We'll fall through and load a VxSchemaElement instead.
60 return new VxSchemaElement(type, name, text, encrypted);
63 protected VxSchemaElement(string newtype, string newname,
64 string newtext, bool newencrypted)
66 _type = newtype;
67 _name = newname;
68 _encrypted = newencrypted;
69 _text = newtext;
72 public static VxSchemaElement create(VxSchemaElement copy)
74 return create(copy.type, copy.name, copy.text, copy.encrypted);
77 public static VxSchemaElement create(IEnumerable<WvAutoCast> _elem)
79 var elem = _elem.GetEnumerator();
80 return create(elem.pop(), elem.pop(), elem.pop(), elem.pop() > 0);
83 public void Write(WvDbusWriter writer)
85 writer.Write(type);
86 writer.Write(name);
87 writer.Write(text);
88 byte encb = (byte)(encrypted ? 1 : 0);
89 writer.Write(encb);
92 public string GetKey()
94 return VxSchema.GetKey(type, name, encrypted);
97 // It's not guaranteed that the text field will be valid SQL. Give
98 // subclasses a chance to translate.
99 public virtual string ToSql()
101 return this.text;
104 // Returns the element's text, along with a header line containing the MD5
105 // sum of the text, and the provided database checksums. This format is
106 // suitable for serializing to disk.
107 public string ToStringWithHeader(VxSchemaChecksum sum)
109 byte[] md5 = MD5.Create().ComputeHash(text.ToUTF8());
111 return String.Format("!!SCHEMAMATIC {0} {1} \r\n{2}",
112 md5.ToHex().ToLower(), sum.GetSumString(), text);
115 public int CompareTo(object obj)
117 if (!(obj is VxSchemaElement))
118 throw new ArgumentException("object is not a VxSchemaElement");
120 VxSchemaElement other = (VxSchemaElement)obj;
121 return GetKey().CompareTo(other.GetKey());
124 public static string GetDbusSignature()
126 return "sssy";
130 // Represents an element of a table, such as a column or index.
131 // Each element has a type, such as "column", and a series of key,value
132 // parameters, such as ("name","MyColumn"), ("type","int"), etc.
133 internal class VxSchemaTableElement
135 public string elemtype;
136 // We can't use a Dictionary<string,string> because we might have repeated
137 // keys (such as two columns for an index).
138 public List<KeyValuePair<string,string>> parameters;
140 public VxSchemaTableElement(string type)
142 elemtype = type;
143 parameters = new List<KeyValuePair<string,string>>();
146 public void AddParam(string name, string val)
148 parameters.Add(new KeyValuePair<string,string>(name, val));
151 // Returns the first parameter found with the given name.
152 // Returns an empty string if none found.
153 public string GetParam(string name)
155 foreach (var kvp in parameters)
156 if (kvp.Key == name)
157 return kvp.Value;
158 return "";
161 // Returns a list of all parameters found with the given name.
162 // Returns an empty list if none found.
163 public List<string> GetParamList(string name)
165 List<string> results = new List<string>();
166 foreach (var kvp in parameters)
167 if (kvp.Key == name)
168 results.Add(kvp.Value);
169 return results;
172 public bool HasDefault()
174 return elemtype == "column" && GetParam("default").ne();
177 // Serializes to "elemtype: key1=value1,key2=value2" format.
178 public override string ToString()
180 StringBuilder sb = new StringBuilder();
181 bool empty = true;
182 sb.Append(elemtype + ": ");
183 foreach (var param in parameters)
185 if (!empty)
186 sb.Append(",");
188 sb.Append(param.Key + "=" + param.Value);
190 empty = false;
192 return sb.ToString();
195 // Returns a string uniquely identifying this element within the table.
196 // Generally includes the element's name, if it has one.
197 public string GetElemKey()
199 if (elemtype == "primary-key")
200 return elemtype;
201 else
202 return elemtype + ": " + GetParam("name");
206 internal class VxSchemaTable : VxSchemaElement,
207 IEnumerable<VxSchemaTableElement>
209 // A list of table elements, so we can maintain the original order
210 private List<VxSchemaTableElement> elems;
211 // A dictionary of table elements, so we can quickly check if we have
212 // an element.
213 private Dictionary<string, VxSchemaTableElement> elemdict;
215 public VxSchemaTable(string newname) :
216 base("Table", newname, null, false)
218 elems = new List<VxSchemaTableElement>();
219 elemdict = new Dictionary<string, VxSchemaTableElement>();
222 public VxSchemaTable(string newname, string newtext) :
223 base("Table", newname, null, false)
225 elems = new List<VxSchemaTableElement>();
226 elemdict = new Dictionary<string, VxSchemaTableElement>();
227 // Parse the new text
228 text = newtext;
231 public VxSchemaTable(VxSchemaElement elem) :
232 base("Table", elem.name, null, false)
234 elems = new List<VxSchemaTableElement>();
235 elemdict = new Dictionary<string, VxSchemaTableElement>();
236 // Parse the new text
237 text = elem.text;
240 public bool Contains(string elemkey)
242 return elemdict.ContainsKey(elemkey);
245 public VxSchemaTableElement this[string elemkey]
249 return elemdict[elemkey];
253 // Implement the IEnumerator interface - just punt to the list
254 IEnumerator IEnumerable.GetEnumerator()
256 return elems.GetEnumerator();
259 public IEnumerator<VxSchemaTableElement> GetEnumerator()
261 return elems.GetEnumerator();
264 public override string text
266 // Other schema elements just store their text verbatim.
267 // We parse it on input and recreate it on output in order to
268 // provide more sensible updating of tables in the database.
271 StringBuilder sb = new StringBuilder();
272 foreach (var elem in elems)
273 sb.Append(elem.ToString() + "\n");
274 return sb.ToString();
278 elems.Clear();
279 elemdict.Clear();
280 char[] equals = {'='};
281 char[] comma = {','};
282 foreach (string line in value.Split('\n'))
284 line.Trim();
285 if (line.Length == 0)
286 continue;
288 string typeseparator = ": ";
289 int index = line.IndexOf(typeseparator);
290 if (index < 0)
291 throw new ArgumentException
292 (wv.fmt("Malformed line in {0}: {1}", key, line));
293 string type = line.Remove(index);
294 string rest = line.Substring(index + typeseparator.Length);
296 var elem = new VxSchemaTableElement(type);
298 foreach (string kvstr in rest.Split(comma))
300 string[] kv = kvstr.Split(equals, 2);
301 if (kv.Length != 2)
302 throw new ArgumentException(wv.fmt(
303 "Invalid entry '{0}' in line '{1}'",
304 kvstr, line));
306 elem.parameters.Add(
307 new KeyValuePair<string,string>(kv[0], kv[1]));
309 Add(elem);
314 public string GetDefaultPKName()
316 return "PK_" + this.name;
319 public string GetDefaultDefaultName(string colname)
321 return wv.fmt("{0}_{1}_default", this.name, colname);
324 // Include any default constraints by, er, default.
325 public string ColumnToSql(VxSchemaTableElement elem)
327 return ColumnToSql(elem, true);
330 public string ColumnToSql(VxSchemaTableElement elem, bool include_default)
332 string colname = elem.GetParam("name");
333 string typename = elem.GetParam("type");
334 string lenstr = elem.GetParam("length");
335 string defval = elem.GetParam("default");
336 string nullstr = elem.GetParam("null");
337 string prec = elem.GetParam("precision");
338 string scale = elem.GetParam("scale");
339 string ident_seed = elem.GetParam("identity_seed");
340 string ident_incr = elem.GetParam("identity_incr");
342 string identstr = "";
343 if (ident_seed.ne() && ident_incr.ne())
344 identstr = wv.fmt(" IDENTITY ({0},{1})", ident_seed, ident_incr);
346 if (nullstr.e())
347 nullstr = "";
348 else if (nullstr == "0")
349 nullstr = " NOT NULL";
350 else
351 nullstr = " NULL";
353 if (lenstr.ne())
354 lenstr = " (" + lenstr + ")";
355 else if (prec.ne() && scale.ne())
356 lenstr = wv.fmt(" ({0},{1})", prec, scale);
358 if (include_default && defval.ne())
360 string defname = GetDefaultDefaultName(colname);
361 defval = " CONSTRAINT " + defname + " DEFAULT " + defval;
363 else
364 defval = "";
366 return wv.fmt("[{0}] [{1}]{2}{3}{4}{5}",
367 colname, typename, lenstr, defval, nullstr, identstr);
370 public string IndexToSql(VxSchemaTableElement elem)
372 List<string> idxcols = elem.GetParamList("column");
373 string idxname = elem.GetParam("name");
374 string unique = elem.GetParam("unique");
375 string clustered = elem.GetParam("clustered") == "1" ?
376 "CLUSTERED " : "";
378 if (unique != "" && unique != "0")
379 unique = "UNIQUE ";
380 else
381 unique = "";
383 return wv.fmt(
384 "CREATE {0}{1}INDEX [{2}] ON [{3}] \n\t({4});",
385 unique, clustered, idxname, this.name, idxcols.join(", "));
388 public string PrimaryKeyToSql(VxSchemaTableElement elem)
390 List<string> idxcols = elem.GetParamList("column");
391 string idxname = elem.GetParam("name");
392 string clustered = elem.GetParam("clustered") == "1" ?
393 " CLUSTERED" : " NONCLUSTERED";
395 if (idxname.e())
396 idxname = GetDefaultPKName();
398 return wv.fmt(
399 "ALTER TABLE [{0}] ADD CONSTRAINT [{1}] PRIMARY KEY{2}\n" +
400 "\t({3});\n\n",
401 this.name, idxname, clustered, idxcols.join(", "));
404 public override string ToSql()
406 List<string> cols = new List<string>();
407 List<string> indexes = new List<string>();
408 string pkey = "";
409 foreach (var elem in elems)
411 if (elem.elemtype == "column")
412 cols.Add(ColumnToSql(elem));
413 else if (elem.elemtype == "index")
414 indexes.Add(IndexToSql(elem));
415 else if (elem.elemtype == "primary-key")
417 if (pkey != "")
419 throw new VxBadSchemaException(
420 "Multiple primary key statements are not " +
421 "permitted in table definitions.\n" +
422 "Conflicting statement: " + elem.ToString() + "\n");
424 pkey = PrimaryKeyToSql(elem);
428 if (cols.Count == 0)
429 throw new VxBadSchemaException("No columns in schema.");
431 string table = String.Format("CREATE TABLE [{0}] (\n\t{1});\n\n{2}{3}\n",
432 name, cols.join(",\n\t"), pkey, indexes.join("\n"));
433 return table;
436 private void Add(VxSchemaTableElement elem)
438 elems.Add(elem);
440 string elemkey = elem.GetElemKey();
442 if (elemdict.ContainsKey(elemkey))
443 throw new VxBadSchemaException(wv.fmt("Duplicate table entry " +
444 "'{0}' found.", elemkey));
446 elemdict.Add(elemkey, elem);
449 public void AddColumn(string name, string type, int isnullable,
450 string len, string defval, string prec, string scale,
451 int isident, string ident_seed, string ident_incr)
453 var elem = new VxSchemaTableElement("column");
454 // FIXME: Put the table name here or not? Might be handy, but could
455 // get out of sync with e.g. filename or whatnot.
456 elem.AddParam("name", name);
457 elem.AddParam("type", type);
458 elem.AddParam("null", isnullable.ToString());
459 if (len.ne())
460 elem.AddParam("length", len);
461 if (defval.ne())
462 elem.AddParam("default", defval);
463 if (prec.ne())
464 elem.AddParam("precision", prec);
465 if (scale.ne())
466 elem.AddParam("scale", scale);
467 if (isident != 0)
469 elem.AddParam("identity_seed", ident_seed);
470 elem.AddParam("identity_incr", ident_incr);
472 Add(elem);
475 public void AddIndex(string name, int unique, int clustered,
476 params string[] columns)
478 WvLog log = new WvLog("AddIndex", WvLog.L.Debug4);
479 log.print("Adding index on {0}, name={1}, unique={2}, clustered={3},\n",
480 columns.join(","), name, unique, clustered);
481 var elem = new VxSchemaTableElement("index");
483 foreach (string col in columns)
484 elem.AddParam("column", col);
485 elem.AddParam("name", name);
486 elem.AddParam("unique", unique.ToString());
487 elem.AddParam("clustered", clustered.ToString());
489 Add(elem);
492 public void AddPrimaryKey(string name, int clustered,
493 params string[] columns)
495 WvLog log = new WvLog("AddPrimaryKey", WvLog.L.Debug4);
496 log.print("Adding primary key '{0}' on {1}, clustered={2}\n",
497 name, columns.join(","), clustered);
498 var elem = new VxSchemaTableElement("primary-key");
500 if (name.ne() && name != GetDefaultPKName())
501 elem.AddParam("name", name);
503 foreach (string col in columns)
504 elem.AddParam("column", col);
505 elem.AddParam("clustered", clustered.ToString());
507 Add(elem);
510 // Figure out what changed between oldtable and newtable.
511 // Returns any deleted elements first, followed by any modified or added
512 // elements in the same order they occur in newtable. Any returned
513 // elements scheduled for changing are from the new table.
514 public static List<KeyValuePair<VxSchemaTableElement, VxDiffType>> GetDiff(
515 VxSchemaTable oldtable, VxSchemaTable newtable)
517 WvLog log = new WvLog("SchemaTable GetDiff", WvLog.L.Debug4);
518 var diff = new List<KeyValuePair<VxSchemaTableElement, VxDiffType>>();
520 foreach (var elem in oldtable.elems)
522 string elemkey = elem.GetElemKey();
523 if (!newtable.Contains(elemkey))
525 log.print("Scheduling {0} for removal.\n", elemkey);
526 diff.Add(new KeyValuePair<VxSchemaTableElement, VxDiffType>(
527 oldtable[elemkey], VxDiffType.Remove));
530 foreach (var elem in newtable.elems)
532 string elemkey = elem.GetElemKey();
533 if (!oldtable.Contains(elemkey))
535 log.print("Scheduling {0} for addition.\n", elemkey);
536 diff.Add(new KeyValuePair<VxSchemaTableElement, VxDiffType>(
537 newtable[elemkey], VxDiffType.Add));
539 else if (elem.ToString() != oldtable[elemkey].ToString())
541 log.print("Scheduling {0} for change.\n", elemkey);
542 diff.Add(new KeyValuePair<VxSchemaTableElement, VxDiffType>(
543 newtable[elemkey], VxDiffType.Change));
547 return diff;
551 // The schema elements for a set of database elements
552 internal class VxSchema : Dictionary<string, VxSchemaElement>
554 public static ISchemaBackend create(string moniker)
556 ISchemaBackend sm = WvMoniker<ISchemaBackend>.create(moniker);
557 if (sm == null && Directory.Exists(moniker))
558 sm = WvMoniker<ISchemaBackend>.create("dir:" + moniker);
559 if (sm == null)
560 sm = WvMoniker<ISchemaBackend>.create("dbi:" + moniker);
561 if (sm == null)
562 throw new Exception
563 (wv.fmt("No moniker found for '{0}'", moniker));
564 return sm;
567 public VxSchema()
571 // Convenience method for making single-element schemas
572 public VxSchema(VxSchemaElement elem)
574 Add(elem.key, elem);
577 public VxSchema(VxSchema copy)
579 foreach (KeyValuePair<string,VxSchemaElement> p in copy)
580 this.Add(p.Key, VxSchemaElement.create(p.Value));
583 public VxSchema(IEnumerable<WvAutoCast> sch)
585 foreach (var row in sch)
587 VxSchemaElement elem = VxSchemaElement.create(row);
588 Add(elem.GetKey(), elem);
592 public void WriteSchema(WvDbusWriter writer)
594 writer.WriteArray(8, this, (w2, p) => {
595 p.Value.Write(w2);
599 // Returns only the elements of the schema that are affected by the diff.
600 // If an element is scheduled to be removed, clear its text field.
601 // Produces a VxSchema that, if sent to a schema backend's Put, will
602 // update the schema as indicated by the diff.
603 public VxSchema GetDiffElements(VxSchemaDiff diff)
605 VxSchema diffschema = new VxSchema();
606 foreach (KeyValuePair<string,VxDiffType> p in diff)
608 if (!this.ContainsKey(p.Key))
609 throw new ArgumentException("The provided diff does not " +
610 "match the schema: extra element '" +
611 (char)p.Value + " " + p.Key + "'");
612 if (p.Value == VxDiffType.Remove)
614 VxSchemaElement elem = VxSchemaElement.create(this[p.Key]);
615 elem.text = "";
616 diffschema[p.Key] = elem;
618 else if (p.Value == VxDiffType.Add || p.Value == VxDiffType.Change)
620 diffschema[p.Key] = VxSchemaElement.create(this[p.Key]);
623 return diffschema;
626 public void Add(string type, string name, string text, bool encrypted)
628 string key = GetKey(type, name, encrypted);
629 if (this.ContainsKey(key))
630 this[key].text += text;
631 else
632 this.Add(key, VxSchemaElement.create(type, name, text, encrypted));
635 public static string GetKey(string type, string name, bool encrypted)
637 string enc_str = encrypted ? "-Encrypted" : "";
638 return String.Format("{0}{1}/{2}", type, enc_str, name);
641 // ParseKey used to live here, but moved to VxSchemaChecksums.
642 public static void ParseKey(string key, out string type, out string name)
644 VxSchemaChecksums.ParseKey(key, out type, out name);
645 return;
648 public static string GetDbusSignature()
650 return String.Format("a({0})", VxSchemaElement.GetDbusSignature());
653 // Make dest look like source. Only copies the bits that need updating.
654 // Note: this is a slightly funny spot to put this method; it really
655 // belongs in ISchemaBackend, but you can't put methods in interfaces.
656 public static VxSchemaErrors CopySchema(ISchemaBackend source,
657 ISchemaBackend dest)
659 return VxSchema.CopySchema(source, dest, VxCopyOpts.None);
662 public static VxSchemaErrors CopySchema(ISchemaBackend source,
663 ISchemaBackend dest, VxCopyOpts opts)
665 WvLog log = new WvLog("CopySchema");
667 if ((opts & VxCopyOpts.ShowProgress) == 0)
668 log = new WvLog("CopySchema", WvLog.L.Debug5);
670 bool show_diff = (opts & VxCopyOpts.ShowDiff) != 0;
671 bool dry_run = (opts & VxCopyOpts.DryRun) != 0;
672 bool destructive = (opts & VxCopyOpts.Destructive) != 0;
674 log.print("Retrieving schema checksums from source.\n");
675 VxSchemaChecksums srcsums = source.GetChecksums();
677 log.print("Retrieving schema checksums from dest.\n");
678 VxSchemaChecksums destsums = dest.GetChecksums();
680 if (srcsums.Count == 0 && destsums.Count != 0)
682 log.print("Source index is empty! " +
683 "Refusing to delete entire database.\n");
684 return new VxSchemaErrors();
687 List<string> names = new List<string>();
689 log.print("Computing diff.\n");
690 VxSchemaDiff diff = new VxSchemaDiff(destsums, srcsums);
692 if (diff.Count == 0)
694 log.print("No changes.\n");
695 return new VxSchemaErrors();
698 if (show_diff)
700 log.print("Changes to apply:\n");
701 log.print(WvLog.L.Info, diff.ToString());
704 log.print("Parsing diff.\n");
705 List<string> to_drop = new List<string>();
706 foreach (KeyValuePair<string,VxDiffType> p in diff)
708 switch (p.Value)
710 case VxDiffType.Remove:
711 to_drop.Add(p.Key);
712 break;
713 case VxDiffType.Add:
714 case VxDiffType.Change:
715 names.Add(p.Key);
716 break;
720 log.print("Retrieving updated schema.\n");
721 VxSchema to_put = source.Get(names);
723 if (dry_run)
724 return new VxSchemaErrors();
726 VxSchemaErrors drop_errs = new VxSchemaErrors();
727 VxSchemaErrors put_errs = new VxSchemaErrors();
729 // We know at least one of to_drop and to_put must have something in
730 // it, otherwise the diff would have been empty.
732 if (to_drop.Count > 0)
734 log.print("Dropping deleted elements.\n");
735 drop_errs = dest.DropSchema(to_drop);
738 VxPutOpts putopts = VxPutOpts.None;
739 if (destructive)
740 putopts |= VxPutOpts.Destructive;
741 if (names.Count > 0)
743 log.print("Updating and adding elements.\n");
744 put_errs = dest.Put(to_put, srcsums, putopts);
747 // Combine the two sets of errors.
748 foreach (var kvp in drop_errs)
749 put_errs.Add(kvp.Key, kvp.Value);
751 return put_errs;