3 * Copyright (C)2007-2008 Versabanq Innovations Inc. and contributors.
4 * See the included file named LICENSE for license information.
7 using System
.Collections
;
8 using System
.Collections
.Generic
;
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
);
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
28 Valid commands: push, pull, dpush, dpull, pascalgen
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
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)
44 static int do_pull(ISchemaBackend remote
, string dir
, VxCopyOpts opts
)
46 log
.print("Pulling schema.\n");
47 VxDiskSchema disk
= new VxDiskSchema(dir
);
50 = VxSchema
.CopySchema(remote
, disk
, opts
| VxCopyOpts
.Verbose
);
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
);
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
);
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
);
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('#');
95 retval
= retval
.Remove(comment_pos
);
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
);
126 Command newcmd
= new Command();
128 newcmd
.pri
= wv
.atoi(parts
[0]);
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)
143 err
.print("Invalid 'export' syntax. " +
144 "('{0}' should be 'where' in '{1}')\n", w
, line
);
147 newcmd
.table
= parts
[2];
148 newcmd
.where
= parts
[4];
152 err
.print("Invalid 'export' syntax. ({0})", line
);
156 else if (newcmd
.cmd
== "zap")
158 if (parts
.Length
< 3)
160 err
.print("Syntax: 'zap <tables...>'. ({0})\n", line
);
163 string[] tables
= line
.Split(whitespace
, 3,
164 StringSplitOptions
.RemoveEmptyEntries
);
165 newcmd
.table
= tables
[2];
169 err
.print("Command '{0}' unknown. ({1})\n", newcmd
.cmd
, line
);
176 static IEnumerable
<Command
> ParseCommands(string[] command_strs
)
178 List
<Command
> commands
= new List
<Command
>();
180 foreach (string _line
in command_strs
)
182 string line
= FixLine(_line
);
183 if (line
.Length
== 0)
186 Command cmd
= ParseCommand(line
);
190 if (last_pri
>= cmd
.pri
)
192 err
.print("Priority code '{0}' <= previous '{1}'. ({2})\n",
193 cmd
.pri
, last_pri
, line
);
203 static int do_dpull(ISchemaBackend remote
, string dir
,
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
);
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
;
225 foreach (string s
in cmd_strings
)
227 if (s
.StartsWith("replace "))
230 replacewith
= s
.Substring(8);
232 //take table.fieldname
233 tablefield
= replacewith
.Substring(0,
234 s
.IndexOf(" with ")-8).Trim().ToLower();
237 replacewith
= replacewith
.Substring(
238 replacewith
.IndexOf(" with ")+6).Trim();
239 if (replacewith
.ToLower() == "null")
240 replaces
.Add(tablefield
,null);
242 replaces
.Add(tablefield
,
243 replacewith
.Substring(1,replacewith
.Length
-2));
247 else if (s
.StartsWith("skipfield "))
249 skipfields
.Add(s
.Substring(10).Trim().ToLower());
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
);
268 if (commands
== null)
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
);
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
))
298 data
.Append(wv
.fmt("DELETE FROM [{0}]\n\n", table
));
301 List
<string> todelete
= new List
<string>();
302 foreach (var p
in dbsums
)
305 VxSchema
.ParseKey(p
.Value
.key
, out type
, out name
);
309 todelete
.Sort(StringComparer
.Ordinal
);
310 foreach (string name
in todelete
)
311 data
.Append(wv
.fmt("DELETE FROM [{0}]\n", name
));
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
);
322 File
.WriteAllBytes(outname
, data
.ToString().ToUTF8());
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
);
338 int dashidx
= info
.Name
.IndexOf('-');
342 string pristr
= info
.Name
.Remove(dashidx
);
343 string rest
= info
.Name
.Substring(dashidx
+ 1);
344 int pri
= wv
.atoi(pristr
);
349 if (info
.Extension
.ToLower() == ".sql")
350 rest
= rest
.Remove(rest
.ToLower().LastIndexOf(".sql"));
360 static int ApplyFiles(string[] files
, ISchemaBackend remote
,
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")
375 log
.print("Applying data from {0}\n", file
);
376 if ((opts
& VxCopyOpts
.DryRun
) != 0)
381 remote
.PutSchemaData(tablename
, data
, seqnum
);
385 // File we didn't generate, try to apply it anyway.
386 remote
.PutSchemaData("", data
, 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
))
405 from f
in Directory
.GetFiles(dir
)
406 where
!f
.EndsWith("~")
408 return ApplyFiles(files
.ToArray(), remote
, opts
);
411 // The Pascal type information for a single stored procedure argument.
414 public string spname
;
415 public string varname
;
416 public List
<string> pascaltypes
;
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
)
431 call
= "m_" + varname
;
433 pascaltypes
= new List
<string>();
435 string type
= arg
.type
.ToLower();
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");
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.
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
);
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")
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" +
536 " result := self;\n" +
538 spname
, varname
, type
, did_first
? "ord(v)" : "v"));
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");
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")
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());
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
);
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
621 + " result := _T{1}.Create(db)\n"
624 classname
, elem
.name
,
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"
640 + " " + memberdecls
.join("\n ") + "\n"
642 + " function MakeRawSql: string; override;\n"
643 + " " + methoddecls
.join("\n ") + "\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
);
658 "function _T{0}.MakeRawSql: string;\n"
660 + " result := TPwDatabase.ExecStr('{0}',\n"
665 argstrs
.join( ",\n "),
666 argcalls
.join(",\n ")));
668 foreach (PascalArg parg
in pargs
)
669 setters
.Add(parg
.GetSetters());
672 var sb
= new StringBuilder();
674 + " * THIS FILE IS AUTOMATICALLY GENERATED BY sm.exe\n"
675 + " * DO NOT EDIT!\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
];
688 log
.print(WvLog
.L
.Error
,
689 "Global symbol '{0}' is never used in any procedure!\n",
693 globalfields
.Add(wv
.fmt("p_{0}: {1};", sym
, type
));
694 globalprops
.Add(wv
.fmt("property {0}: {1} read p_{0} write p_{0};",
698 sb
.Append("interface\n\n"
699 + "uses Classes, uPwData;\n"
703 + " " + types
.join("\n ")
705 + " " + classname
+ " = class(TComponent)\n"
707 + " fDb: TPwDatabase;\n"
708 + " " + globalfields
.join("\n ") + "\n"
710 + " property db: TPwDatabase read fDb write fDb;\n"
711 + " " + globalprops
.join("\n ") + "\n"
713 + " constructor Create(db: TPwDatabase); reintroduce;\n"
714 + " " + iface
.join(" ")
717 sb
.Append("implementation\n"
719 + "constructor " + classname
+ ".Create(db: TPwDatabase);\n"
721 + " inherited Create(db);\n"
722 + " self.db := db;\n"
728 sb
.Append(setters
.join(""));
730 sb
.Append("\n\nend.\n");
733 Console
.Write(sb
.ToString());
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
)
758 catch (Exception e
) {
759 wv
.printerr("schemamatic: {0}\n", e
.Message
);
764 public static int pascalgen_Main(List
<string> extra
,
765 Dictionary
<string,string> global_syms
,
768 if (extra
.Count
!= 3)
774 string classname
= extra
[1];
775 string dir
= extra
[2];
777 do_pascalgen(classname
, dir
, global_syms
, outfile
);
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
= {',', ';', ':'}
;
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; }
)
804 WvLog
.maxlevel
= (WvLog
.L
)verbose
;
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] : ".";
829 // look up a bookmark if it exists, else use the provided name as a
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
);
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));
853 } catch (IOException
) {
854 // not a big deal if we can't save our bookmarks.
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
);
869 Console
.Error
.WriteLine("\nUnknown command '{0}'\n", cmd
);