DON'T USE THIS REPO: IT'S OBSOLETE.
[versaplex.git] / versaplexd / sm.cs
blob0973fbf61ba92861acb179cbe47ac78291adb35e
1 /*
2 * Versaplex:
3 * Copyright (C)2007-2008 Versabanq Innovations Inc. and contributors.
4 * See the included file named LICENSE for license information.
5 */
6 using System;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.IO;
10 using System.Text;
11 using System.Linq;
12 using Wv;
13 using Wv.Extensions;
14 using Wv.NDesk.Options;
16 public static class SchemamaticCli
18 static WvLog log = new WvLog("sm");
19 static WvLog err = log.split(WvLog.L.Error);
21 static int ShowHelp()
23 Console.Error.WriteLine(
24 @"Usage: sm [--dry-run] [--force] [--verbose] <command> <moniker> <dir>
25 Schemamatic: copy database schemas between a database server and the
26 current directory.
28 Valid commands: push, pull, dpush, dpull, pascalgen
30 pascalgen usage:
31 sm [--verbose] [--global-syms=<syms>] pascalgen <classname> <dir>
33 --dry-run/-n: lists the files that would be changed but doesn't modify them.
34 --force/-f: performs potentially destructive database update operations.
35 --verbose/-v: Increase the verbosity of the log output. Can be specified
36 multiple times.
37 --global-syms/-g: Move <syms> from parameters to members. Symbols can be
38 separated by any one of comma, semicolon, or colon, or this option
39 can be specified multiple times. (pascalgen only)
40 ");
41 return 99;
44 static int do_pull(ISchemaBackend remote, string dir, VxCopyOpts opts)
46 log.print("Pulling schema.\n");
47 VxDiskSchema disk = new VxDiskSchema(dir);
49 VxSchemaErrors errs
50 = VxSchema.CopySchema(remote, disk, opts | VxCopyOpts.Verbose);
52 int code = 0;
53 foreach (var p in errs)
55 foreach (var err in p.Value)
57 Console.WriteLine("{0} applying {1}: {2} ({3})",
58 err.level, err.key, err.msg, err.errnum);
59 code = 1;
63 return code;
66 static int do_push(ISchemaBackend remote, string dir, VxCopyOpts opts)
68 log.print("Pushing schema.\n");
69 VxDiskSchema disk = new VxDiskSchema(dir);
71 VxSchemaErrors errs = VxSchema.CopySchema(disk, remote, opts);
73 int code = 0;
74 foreach (var p in errs)
76 foreach (var err in p.Value)
78 Console.WriteLine("{0} applying {1}: {2} ({3})",
79 err.level, err.key, err.msg, err.errnum);
80 code = 1;
84 return code;
87 static char[] whitespace = {' ', '\t'};
89 // Remove any leading or trailing whitespace, and any comments.
90 static string FixLine(string line)
92 string retval = line.Trim();
93 int comment_pos = retval.IndexOf('#');
94 if (comment_pos >= 0)
95 retval = retval.Remove(comment_pos);
97 return retval;
100 public class Command
102 public int pri;
103 public string cmd;
104 public string table;
105 public string where;
108 // Parses a command line of the form "sequence_num command table ..."
109 // A sequence number is a 5-digit integer, zero-padded if needed.
110 // The current commands are:
111 // zap - "00001 zap table_name" or "00002 zap *"
112 // Adds table_name to the list of tables to clear in 00001-zap.sql
113 // If the table name is *, zaps all tables.
114 // export - "00003 export foo" or "00004 export foo where condition"
115 static Command ParseCommand(string line)
117 string[] parts = line.Split(whitespace, 5,
118 StringSplitOptions.RemoveEmptyEntries);
120 if (parts.Length < 2)
122 err.print("Invalid command. ({0})\n", line);
123 return null;
126 Command newcmd = new Command();
128 newcmd.pri = wv.atoi(parts[0]);
129 if (newcmd.pri < 0)
130 return null;
132 newcmd.cmd = parts[1].ToLower();
133 if (newcmd.cmd == "export")
135 // You can say "12345 export foo" or "12345 export foo where bar"
136 if (parts.Length == 3)
137 newcmd.table = parts[2];
138 else if (parts.Length == 5)
140 string w = parts[3];
141 if (w != "where")
143 err.print("Invalid 'export' syntax. " +
144 "('{0}' should be 'where' in '{1}')\n", w, line);
145 return null;
147 newcmd.table = parts[2];
148 newcmd.where = parts[4];
150 else
152 err.print("Invalid 'export' syntax. ({0})", line);
153 return null;
156 else if (newcmd.cmd == "zap")
158 if (parts.Length < 3)
160 err.print("Syntax: 'zap <tables...>'. ({0})\n", line);
161 return null;
163 string[] tables = line.Split(whitespace, 3,
164 StringSplitOptions.RemoveEmptyEntries);
165 newcmd.table = tables[2];
167 else
169 err.print("Command '{0}' unknown. ({1})\n", newcmd.cmd, line);
170 return null;
173 return newcmd;
176 static IEnumerable<Command> ParseCommands(string[] command_strs)
178 List<Command> commands = new List<Command>();
179 int last_pri = -1;
180 foreach (string _line in command_strs)
182 string line = FixLine(_line);
183 if (line.Length == 0)
184 continue;
186 Command cmd = ParseCommand(line);
187 if (cmd == null)
188 return null;
190 if (last_pri >= cmd.pri)
192 err.print("Priority code '{0}' <= previous '{1}'. ({2})\n",
193 cmd.pri, last_pri, line);
194 return null;
196 last_pri = cmd.pri;
198 commands.Add(cmd);
200 return commands;
203 static int do_dpull(ISchemaBackend remote, string dir,
204 VxCopyOpts opts)
206 log.print("Pulling data.\n");
208 string cmdfile = Path.Combine(dir, "data-export.txt");
209 if (!File.Exists(cmdfile))
211 err.print("Missing command file: {0}\n", cmdfile);
212 return 5;
215 log.print("Retrieving schema checksums from database.\n");
216 VxSchemaChecksums dbsums = remote.GetChecksums();
218 log.print("Reading export commands.\n");
219 string[] cmd_strings = File.ReadAllLines(cmdfile);
221 Dictionary<string,string> replaces = new Dictionary<string,string>();
222 List<string> skipfields = new List<string>();
223 string tablefield, replacewith;
224 int i = 0;
225 foreach (string s in cmd_strings)
227 if (s.StartsWith("replace "))
229 //take replace off
230 replacewith = s.Substring(8);
232 //take table.fieldname
233 tablefield = replacewith.Substring(0,
234 s.IndexOf(" with ")-8).Trim().ToLower();
236 //take the value
237 replacewith = replacewith.Substring(
238 replacewith.IndexOf(" with ")+6).Trim();
239 if (replacewith.ToLower() == "null")
240 replaces.Add(tablefield,null);
241 else
242 replaces.Add(tablefield,
243 replacewith.Substring(1,replacewith.Length-2));
245 cmd_strings[i] = "";
247 else if (s.StartsWith("skipfield "))
249 skipfields.Add(s.Substring(10).Trim().ToLower());
250 cmd_strings[i] = "";
253 i++;
256 log.print("Parsing commands.\n");
257 IEnumerable<Command> commands = ParseCommands(cmd_strings);
259 foreach (Command cmd in commands)
261 if (cmd.cmd != "zap" && !dbsums.ContainsKey("Table/" + cmd.table))
263 err.print("Table doesn't exist: {0}\n", cmd.table);
264 return 4;
268 if (commands == null)
269 return 3;
271 string datadir = Path.Combine(dir, "DATA");
272 log.print("Cleaning destination directory '{0}'.\n", datadir);
273 Directory.CreateDirectory(datadir);
274 foreach (string path in Directory.GetFiles(datadir, "*.sql"))
276 if ((opts & VxCopyOpts.DryRun) != 0)
277 log.print("Would have deleted '{0}'\n", path);
278 else
279 File.Delete(path);
282 log.print("Processing commands.\n");
283 foreach (Command cmd in commands)
285 StringBuilder data = new StringBuilder();
286 if (cmd.cmd == "export")
288 data.Append(wv.fmt("TABLE {0}\n", cmd.table));
289 data.Append(remote.GetSchemaData(cmd.table, cmd.pri, cmd.where,
290 replaces, skipfields));
292 else if (cmd.cmd == "zap")
294 foreach (string table in cmd.table.Split(whitespace,
295 StringSplitOptions.RemoveEmptyEntries))
297 if (table != "*")
298 data.Append(wv.fmt("DELETE FROM [{0}]\n\n", table));
299 else
301 List<string> todelete = new List<string>();
302 foreach (var p in dbsums)
304 string type, name;
305 VxSchema.ParseKey(p.Value.key, out type, out name);
306 if (type == "Table")
307 todelete.Add(name);
309 todelete.Sort(StringComparer.Ordinal);
310 foreach (string name in todelete)
311 data.Append(wv.fmt("DELETE FROM [{0}]\n", name));
314 cmd.table = "ZAP";
317 string outname = Path.Combine(datadir,
318 wv.fmt("{0:d5}-{1}.sql", cmd.pri, cmd.table));
319 if ((opts & VxCopyOpts.DryRun) != 0)
320 log.print("Would have written '{0}'\n", outname);
321 else
322 File.WriteAllBytes(outname, data.ToString().ToUTF8());
325 return 0;
328 // Extracts the table name and priority out of a path. E.g. for
329 // "/foo/12345-bar.sql", returns "12345" and "bar" as the priority and
330 // table name. Returns -1/null if the parse fails.
331 static void ParsePath(string pathname, out int seqnum,
332 out string tablename)
334 FileInfo info = new FileInfo(pathname);
335 seqnum = -1;
336 tablename = null;
338 int dashidx = info.Name.IndexOf('-');
339 if (dashidx < 0)
340 return;
342 string pristr = info.Name.Remove(dashidx);
343 string rest = info.Name.Substring(dashidx + 1);
344 int pri = wv.atoi(pristr);
346 if (pri < 0)
347 return;
349 if (info.Extension.ToLower() == ".sql")
350 rest = rest.Remove(rest.ToLower().LastIndexOf(".sql"));
351 else
352 return;
354 seqnum = pri;
355 tablename = rest;
357 return;
360 static int ApplyFiles(string[] files, ISchemaBackend remote,
361 VxCopyOpts opts)
363 int seqnum;
364 string tablename;
366 Array.Sort(files);
367 foreach (string file in files)
369 string data = File.ReadAllText(file, Encoding.UTF8);
370 ParsePath(file, out seqnum, out tablename);
372 if (tablename == "ZAP")
373 tablename = "";
375 log.print("Applying data from {0}\n", file);
376 if ((opts & VxCopyOpts.DryRun) != 0)
377 continue;
379 if (seqnum > 0)
381 remote.PutSchemaData(tablename, data, seqnum);
383 else
385 // File we didn't generate, try to apply it anyway.
386 remote.PutSchemaData("", data, 0);
390 return 0;
393 static int do_dpush(ISchemaBackend remote, string dir, VxCopyOpts opts)
395 log.print("Pushing data.\n");
397 if (!Directory.Exists(dir))
398 return 5; // nothing to do
400 string datadir = Path.Combine(dir, "DATA");
401 if (Directory.Exists(datadir))
402 dir = datadir;
404 var files =
405 from f in Directory.GetFiles(dir)
406 where !f.EndsWith("~")
407 select f;
408 return ApplyFiles(files.ToArray(), remote, opts);
411 // The Pascal type information for a single stored procedure argument.
412 struct PascalArg
414 public string spname;
415 public string varname;
416 public List<string> pascaltypes;
417 public string call;
418 public string defval;
420 // Most of the time we just care about the first pascal type.
421 // Any other types are helper types to make convenience functions.
422 public string pascaltype
424 get { return pascaltypes[0]; }
427 public PascalArg(string _spname, SPArg arg)
429 spname = _spname;
430 varname = arg.name;
431 call = "m_" + varname;
432 defval = arg.defval;
433 pascaltypes = new List<string>();
435 string type = arg.type.ToLower();
436 string nulldef;
437 if (type.Contains("char") || type.Contains("text") ||
438 type.Contains("binary") || type.Contains("sysname"))
440 pascaltypes.Add("string");
441 nulldef = "'__!NIL!__'";
443 else if (type.Contains("int"))
445 pascaltypes.Add("integer");
446 nulldef = "low(integer)+42";
448 else if (type.Contains("bit") || type.Contains("bool"))
450 pascaltypes.Add("integer");
451 pascaltypes.Add("boolean");
452 nulldef = "low(integer)+42";
454 else if (type.Contains("float") || type.Contains("decimal") ||
455 type.Contains("real") || type.Contains("numeric"))
457 pascaltypes.Add("double");
458 nulldef = "1e-42";
460 else if (type.Contains("date") || type.Contains("time"))
462 pascaltypes.Add("TDateTime");
463 call = wv.fmt("TPwDateWrap.Create(m_{0})", arg.name);
464 nulldef = "low(integer)+42";
465 // Non-null default dates not supported.
466 if (defval.ne())
467 defval = "";
469 else if (type.Contains("money"))
471 pascaltypes.Add("currency");
472 nulldef = "-90000000000.0042";
474 else if (type.Contains("image"))
476 pascaltypes.Add("TBlobField");
477 call = wv.fmt("TBlobField(m_{0})", varname);
478 nulldef = "nil";
480 else
482 throw new ArgumentException(wv.fmt(
483 "Unknown parameter type '{0}' (parameter @{1})",
484 arg.type, arg.name));
487 if (defval.e() || defval.ToLower() == "null")
488 defval = nulldef;
491 public string GetMemberDecl()
493 // The first type is the main one, and gets the declaration.
494 return wv.fmt("m_{0}: {1};", varname, pascaltypes[0]);
497 public string GetMethodDecl()
499 var l = new List<string>();
500 foreach (string type in pascaltypes)
502 l.Add(wv.fmt("function set{0}(v: {1}): _T{2}; {3}inline;",
503 varname, type, spname,
504 pascaltypes.Count > 1 ? "overload; " : ""));
506 return l.join("\n ");
509 public string GetDecl()
511 string extra = defval.ne() ? " = " + defval : "";
513 return "p_" + varname + ": " + pascaltypes[0] + extra;
516 public string GetDefine()
518 return "p_" + varname + ": " + pascaltypes[0];
521 public string GetCtor()
523 return wv.fmt(".set{0}(p_{0})", varname);
526 public string GetSetters()
528 var sb = new StringBuilder();
529 bool did_first = false;
530 foreach (string type in pascaltypes)
532 // Types past the first get sent through ord()
533 sb.Append(wv.fmt("function _T{0}.set{1}(v: {2}): _T{0};\n" +
534 "begin\n" +
535 " m_{1} := {3};\n" +
536 " result := self;\n" +
537 "end;\n\n",
538 spname, varname, type, did_first ? "ord(v)" : "v"));
539 did_first = true;
541 return sb.ToString();
545 static void do_pascalgen(string classname, string dir,
546 Dictionary<string,string> global_syms, string outfile)
548 if (!classname.StartsWith("T"))
550 Console.Error.Write("Classname must start with T.\n");
551 return;
554 Console.Error.Write("Generating Pascal file...\n");
556 // Replace leading 'T' with a 'u'
557 string unitname = "u" + classname.Remove(0, 1);
559 VxDiskSchema disk = new VxDiskSchema(dir);
560 VxSchema schema = disk.Get(null);
562 var types = new List<string>();
563 var iface = new List<string>();
564 var impl = new List<string>();
565 var setters = new List<string>();
567 var keys = schema.Keys.ToList();
568 keys.Sort(StringComparer.Ordinal);
569 foreach (var key in keys)
571 var elem = schema[key];
572 if (elem.type != "Procedure")
573 continue;
575 var sp = new StoredProcedure(elem.text);
577 var pargs = new List<PascalArg>();
578 foreach (SPArg arg in sp.args)
579 pargs.Add(new PascalArg(elem.name, arg));
581 var decls = new List<string>();
582 var impls = new List<string>();
583 var ctors = new List<string>();
584 foreach (PascalArg parg in pargs)
586 if (!global_syms.ContainsKey(parg.varname.ToLower()))
588 decls.Add(parg.GetDecl());
589 impls.Add(parg.GetDefine());
590 ctors.Add(parg.GetCtor());
592 else
594 string old = global_syms[parg.varname.ToLower()];
595 if (old.ne() && old.ToLower() != parg.pascaltype.ToLower())
597 log.print("Definition for global '{0}' differs! " +
598 "old '{1}', new '{2}'\n",
599 parg.varname, old, parg.pascaltype);
601 else
603 // The global declaration supplants the local
604 // declaration and implementation, but not the ctor.
605 global_syms[parg.varname.ToLower()] = parg.pascaltype;
606 ctors.Add(parg.GetCtor());
611 // Factory function that produces builder objects
612 iface.Add(wv.fmt("function {0}\n"
613 + " ({1}): _T{0};\n",
614 elem.name, decls.join(";\n ")));
616 // Actual implementation of the factory function
617 impl.Add(wv.fmt(
618 "function {0}.{1}\n"
619 + " ({2}): _T{1};\n"
620 + "begin\n"
621 + " result := _T{1}.Create(db)\n"
622 + " {3};\n"
623 + "end;\n\n",
624 classname, elem.name,
625 impls.join(";\n "),
626 ctors.join("\n ")
629 var memberdecls = new List<string>();
630 var methoddecls = new List<string>();
631 foreach (PascalArg parg in pargs)
633 memberdecls.Add(parg.GetMemberDecl());
634 methoddecls.Add(parg.GetMethodDecl());
637 // Declaration for per-procedure builder class
638 types.Add("_T" + elem.name + " = class(TPwDataCmd)\n"
639 + " private\n"
640 + " " + memberdecls.join("\n ") + "\n"
641 + " public\n"
642 + " function MakeRawSql: string; override;\n"
643 + " " + methoddecls.join("\n ") + "\n"
644 + " end;\n"
647 // Member functions of the builder classes
649 var argstrs = new List<string>();
650 var argcalls = new List<string>();
651 foreach (PascalArg parg in pargs)
653 argstrs.Add(wv.fmt("'{0}'", parg.varname));
654 argcalls.Add(parg.call);
657 setters.Add(wv.fmt(
658 "function _T{0}.MakeRawSql: string;\n"
659 + "begin\n"
660 + " result := TPwDatabase.ExecStr('{0}',\n"
661 + " [{1}],\n"
662 + " [{2}]);\n"
663 + "end;\n\n",
664 elem.name,
665 argstrs.join( ",\n "),
666 argcalls.join(",\n ")));
668 foreach (PascalArg parg in pargs)
669 setters.Add(parg.GetSetters());
672 var sb = new StringBuilder();
673 sb.Append("(*\n"
674 + " * THIS FILE IS AUTOMATICALLY GENERATED BY sm.exe\n"
675 + " * DO NOT EDIT!\n"
676 + " *)\n"
677 + "unit " + unitname + ";\n\n");
679 var global_syms_keys = global_syms.Keys.ToList();
680 global_syms_keys.Sort();
681 var globalfields = new List<string>();
682 var globalprops = new List<string>();
683 foreach (var sym in global_syms_keys)
685 string type = global_syms[sym];
686 if (type.e())
688 log.print(WvLog.L.Error,
689 "Global symbol '{0}' is never used in any procedure!\n",
690 sym);
691 return;
693 globalfields.Add(wv.fmt("p_{0}: {1};", sym, type));
694 globalprops.Add(wv.fmt("property {0}: {1} read p_{0} write p_{0};",
695 sym, type));
698 sb.Append("interface\n\n"
699 + "uses Classes, uPwData;\n"
700 + "\n"
701 + "{$M+}\n"
702 + "type\n"
703 + " " + types.join("\n ")
704 + " \n"
705 + " " + classname + " = class(TComponent)\n"
706 + " private\n"
707 + " fDb: TPwDatabase;\n"
708 + " " + globalfields.join("\n ") + "\n"
709 + " published\n"
710 + " property db: TPwDatabase read fDb write fDb;\n"
711 + " " + globalprops.join("\n ") + "\n"
712 + " public\n"
713 + " constructor Create(db: TPwDatabase); reintroduce;\n"
714 + " " + iface.join(" ")
715 + " end;\n\n");
717 sb.Append("implementation\n"
718 + "\n"
719 + "constructor " + classname + ".Create(db: TPwDatabase);\n"
720 + "begin\n"
721 + " inherited Create(db);\n"
722 + " self.db := db;\n"
723 + "end;\n"
724 + "\n"
725 + impl.join("")
728 sb.Append(setters.join(""));
730 sb.Append("\n\nend.\n");
732 if (outfile.e())
733 Console.Write(sb.ToString());
734 else
736 Console.Error.Write("Writing file: {0}\n", outfile);
737 using (var f = new FileStream(outfile,
738 FileMode.Create, FileAccess.Write))
740 f.write(sb.ToUTF8());
744 Console.Error.Write("Done.\n");
747 private static ISchemaBackend GetBackend(WvUrl url)
749 log.print("Connecting to '{0}'\n", url);
750 return VxSchema.create(url.ToString(true));
753 public static int Main(string[] args)
755 try {
756 return _Main(args);
758 catch (Exception e) {
759 wv.printerr("schemamatic: {0}\n", e.Message);
760 return 99;
764 public static int pascalgen_Main(List<string> extra,
765 Dictionary<string,string> global_syms,
766 string outfile)
768 if (extra.Count != 3)
770 ShowHelp();
771 return 98;
774 string classname = extra[1];
775 string dir = extra[2];
777 do_pascalgen(classname, dir, global_syms, outfile);
778 return 0;
781 public static int _Main(string[] args)
783 // command line options
784 VxCopyOpts opts = VxCopyOpts.Verbose;
786 int verbose = (int)WvLog.L.Info;
787 string outfile = null;
788 var global_syms = new Dictionary<string,string>();
789 var extra = new OptionSet()
790 .Add("n|dry-run", delegate(string v) { opts |= VxCopyOpts.DryRun; } )
791 .Add("f|force", delegate(string v) { opts |= VxCopyOpts.Destructive; } )
792 .Add("v|verbose", delegate(string v) { verbose++; } )
793 .Add("g|global-sym=", delegate(string v)
795 var splitopts = StringSplitOptions.RemoveEmptyEntries;
796 char[] splitchars = {',', ';', ':'};
797 if (v.ne())
798 foreach (var sym in v.Split(splitchars, splitopts))
799 global_syms.Add(sym.ToLower(), null);
801 .Add("o|output-file=", delegate(string v) { outfile = v; })
802 .Parse(args);
804 WvLog.maxlevel = (WvLog.L)verbose;
806 if (extra.Count < 1)
808 ShowHelp();
809 return 97;
812 string cmd = extra[0];
813 if (cmd == "pascalgen")
814 return pascalgen_Main(extra, global_syms, outfile);
816 WvIni bookmarks = new WvIni(
817 wv.PathCombine(wv.getenv("HOME"), ".wvdbi.ini"));
819 string moniker = extra.Count > 1
820 ? extra[1] : bookmarks.get("Defaults", "url", null);
821 string dir = extra.Count > 2 ? extra[2] : ".";
823 if (moniker.e())
825 ShowHelp();
826 return 96;
829 // look up a bookmark if it exists, else use the provided name as a
830 // moniker
831 moniker = bookmarks.get("Bookmarks", moniker, moniker);
833 // provide a default username/password if they weren't provided
834 // FIXME: are there URLs that should have a blank username/password?
835 WvUrl url = new WvUrl(moniker);
836 if (url.user.e())
837 url.user = bookmarks.get("Defaults", "user");
838 if (url.password.e())
839 url.password = bookmarks.get("Defaults", "password");
841 using (var backend = GetBackend(url))
843 bookmarks.set("Defaults", "url", url.ToString(true));
844 bookmarks.maybeset("Defaults", "user", url.user);
845 bookmarks.maybeset("Defaults", "password", url.password);
847 string p = url.path.StartsWith("/")
848 ? url.path.Substring(1) : url.path;
849 bookmarks.set("Bookmarks", p, url.ToString(true));
851 try {
852 bookmarks.save();
853 } catch (IOException) {
854 // not a big deal if we can't save our bookmarks.
857 if (cmd == "remote")
858 return 0; // just saved the bookmark, so we're done
859 else if (cmd == "pull")
860 do_pull(backend, dir, opts);
861 else if (cmd == "push")
862 do_push(backend, dir, opts);
863 else if (cmd == "dpull")
864 do_dpull(backend, dir, opts);
865 else if (cmd == "dpush")
866 do_dpush(backend, dir, opts);
867 else
869 Console.Error.WriteLine("\nUnknown command '{0}'\n", cmd);
870 ShowHelp();
874 return 0;