Transport no longer has a Stream member.
[versaplex.git] / versaplexd / sm.cs
blob2bd268b6940a15057237a3cba1b745cee60bcbf6
1 using System;
2 using System.Collections.Generic;
3 using System.IO;
4 using System.Text;
5 using System.Linq;
6 using Wv;
7 using Wv.Extensions;
8 using Wv.NDesk.Options;
10 public static class SchemamaticCli
12 static WvLog log = new WvLog("sm");
13 static WvLog err = log.split(WvLog.L.Error);
15 static int ShowHelp()
17 Console.Error.WriteLine(
18 @"Usage: sm [--dry-run] <push|pull|dpush|dpull> <moniker> <dir>
19 Schemamatic: copy database schemas between a database server and the
20 current directory.
22 --dry-run: lists the files that would be changed but doesn't modify them.
23 ");
24 return 99;
27 static int DoExport(ISchemaBackend remote, string dir, VxCopyOpts opts)
29 log.print("Pulling schema.\n");
30 VxDiskSchema disk = new VxDiskSchema(dir);
32 VxSchemaErrors errs
33 = VxSchema.CopySchema(remote, disk, opts | VxCopyOpts.Verbose);
35 int code = 0;
36 foreach (var p in errs)
37 foreach (var err in p.Value)
39 Console.WriteLine("Error applying {0}: {1} ({2})",
40 err.key, err.msg, err.errnum);
41 code = 1;
44 return code;
47 static int DoApply(ISchemaBackend remote, string dir, VxCopyOpts opts)
49 log.print("Pushing schema.\n");
50 VxDiskSchema disk = new VxDiskSchema(dir);
52 VxSchemaErrors errs = VxSchema.CopySchema(disk, remote, opts);
54 int code = 0;
55 foreach (var p in errs)
56 foreach (var err in p.Value)
58 Console.WriteLine("Error applying {0}: {1} ({2})",
59 err.key, err.msg, err.errnum);
60 code = 1;
63 return code;
66 static char[] whitespace = {' ', '\t'};
68 // Remove any leading or trailing whitespace, and any comments.
69 static string FixLine(string line)
71 string retval = line.Trim();
72 int comment_pos = retval.IndexOf('#');
73 if (comment_pos >= 0)
74 retval = retval.Remove(comment_pos);
76 return retval;
79 public class Command
81 public int pri;
82 public string cmd;
83 public string table;
84 public string where;
87 // Parses a command line of the form "sequence_num command table ..."
88 // A sequence number is a 5-digit integer, zero-padded if needed.
89 // The current commands are:
90 // zap - "00001 zap table_name" or "00002 zap *"
91 // Adds table_name to the list of tables to clear in 00001-zap.sql
92 // If the table name is *, zaps all tables.
93 // export - "00003 export foo" or "00004 export foo where condition"
94 static Command ParseCommand(string line)
96 string[] parts = line.Split(whitespace, 5,
97 StringSplitOptions.RemoveEmptyEntries);
99 if (parts.Length < 2)
101 err.print("Invalid command. ({0})\n", line);
102 return null;
105 Command newcmd = new Command();
107 newcmd.pri = wv.atoi(parts[0]);
108 if (newcmd.pri < 0)
109 return null;
111 newcmd.cmd = parts[1];
112 if (newcmd.cmd == "export")
114 // You can say "12345 export foo" or "12345 export foo where bar"
115 if (parts.Length == 3)
116 newcmd.table = parts[2];
117 else if (parts.Length == 5)
119 string w = parts[3];
120 if (w != "where")
122 err.print("Invalid 'export' syntax. " +
123 "('{0}' should be 'where' in '{1}')\n", w, line);
124 return null;
126 newcmd.table = parts[2];
127 newcmd.where = parts[4];
129 else
131 err.print("Invalid 'export' syntax. ({0})", line);
132 return null;
135 else if (newcmd.cmd == "zap")
137 if (parts.Length < 3)
139 err.print("Syntax: 'zap <tables...>'. ({0})\n", line);
140 return null;
142 string[] tables = line.Split(whitespace, 3,
143 StringSplitOptions.RemoveEmptyEntries);
144 newcmd.table = tables[2];
146 else
148 err.print("Command '{0}' unknown. ({1})\n", newcmd.cmd, line);
149 return null;
152 return newcmd;
155 static IEnumerable<Command> ParseCommands(string[] command_strs)
157 List<Command> commands = new List<Command>();
158 int last_pri = -1;
159 foreach (string _line in command_strs)
161 string line = FixLine(_line);
162 if (line.Length == 0)
163 continue;
165 Command cmd = ParseCommand(line);
166 if (cmd == null)
167 return null;
169 if (last_pri >= cmd.pri)
171 err.print("Priority code '{0}' <= previous '{1}'. ({2})\n",
172 cmd.pri, last_pri, line);
173 return null;
175 last_pri = cmd.pri;
177 commands.Add(cmd);
179 return commands;
182 static int DoGetData(ISchemaBackend remote, string dir,
183 VxCopyOpts opts)
185 log.print("Pulling data.\n");
187 string cmdfile = Path.Combine(dir, "data-export.txt");
188 if (!File.Exists(cmdfile))
190 err.print("Missing command file: {0}\n", cmdfile);
191 return 5;
194 log.print("Retrieving schema checksums from database.\n");
195 VxSchemaChecksums dbsums = remote.GetChecksums();
197 log.print("Reading export commands.\n");
198 string[] cmd_strings = File.ReadAllLines(cmdfile);
200 log.print("Parsing commands.\n");
201 IEnumerable<Command> commands = ParseCommands(cmd_strings);
203 foreach (Command cmd in commands)
205 if (cmd.cmd != "zap" && !dbsums.ContainsKey("Table/" + cmd.table))
207 err.print("Table doesn't exist: {0}\n", cmd.table);
208 return 4;
212 if (commands == null)
213 return 3;
215 string datadir = Path.Combine(dir, "DATA");
216 log.print("Cleaning destination directory '{0}'.\n", datadir);
217 Directory.CreateDirectory(datadir);
218 foreach (string path in Directory.GetFiles(datadir, "*.sql"))
220 if ((opts & VxCopyOpts.DryRun) != 0)
221 log.print("Would have deleted '{0}'\n", path);
222 else
223 File.Delete(path);
226 log.print("Processing commands.\n");
227 foreach (Command cmd in commands)
229 StringBuilder data = new StringBuilder();
230 if (cmd.cmd == "export")
232 data.Append(wv.fmt("DELETE FROM [{0}];\n", cmd.table));
233 data.Append(remote.GetSchemaData(cmd.table, cmd.pri, cmd.where));
235 else if (cmd.cmd == "zap")
237 foreach (string table in cmd.table.Split(whitespace,
238 StringSplitOptions.RemoveEmptyEntries))
240 if (table != "*")
241 data.Append(wv.fmt("DELETE FROM [{0}]\n\n", table));
242 else
244 List<string> todelete = new List<string>();
245 foreach (var p in dbsums)
247 string type, name;
248 VxSchema.ParseKey(p.Value.key, out type, out name);
249 if (type == "Table")
250 todelete.Add(name);
252 todelete.Sort();
253 foreach (string name in todelete)
254 data.Append(wv.fmt("DELETE FROM [{0}]\n", name));
257 cmd.table = "ZAP";
260 string outname = Path.Combine(datadir,
261 wv.fmt("{0:d5}-{1}.sql", cmd.pri, cmd.table));
262 if ((opts & VxCopyOpts.DryRun) != 0)
263 log.print("Would have written '{0}'\n", outname);
264 else
265 File.WriteAllBytes(outname, data.ToString().ToUTF8());
268 return 0;
271 // Extracts the table name and priority out of a path. E.g. for
272 // "/foo/12345-bar.sql", returns "12345" and "bar" as the priority and
273 // table name. Returns -1/null if the parse fails.
274 static void ParsePath(string pathname, out int seqnum,
275 out string tablename)
277 FileInfo info = new FileInfo(pathname);
278 seqnum = -1;
279 tablename = null;
281 int dashidx = info.Name.IndexOf('-');
282 if (dashidx < 0)
283 return;
285 string pristr = info.Name.Remove(dashidx);
286 string rest = info.Name.Substring(dashidx + 1);
287 int pri = wv.atoi(pristr);
289 if (pri < 0)
290 return;
292 if (info.Extension.ToLower() == ".sql")
293 rest = rest.Remove(rest.ToLower().LastIndexOf(".sql"));
294 else
295 return;
297 seqnum = pri;
298 tablename = rest;
300 return;
303 static int ApplyFiles(string[] files, ISchemaBackend remote,
304 VxCopyOpts opts)
306 int seqnum;
307 string tablename;
309 Array.Sort(files);
310 foreach (string file in files)
312 string data = File.ReadAllText(file, System.Text.Encoding.UTF8);
313 ParsePath(file, out seqnum, out tablename);
315 if (tablename == "ZAP")
316 tablename = "";
318 log.print("Applying data from {0}\n", file);
319 if ((opts & VxCopyOpts.DryRun) != 0)
320 continue;
322 if (seqnum > 0)
324 remote.PutSchemaData(tablename, data, seqnum);
326 else
328 // File we didn't generate, try to apply it anyway.
329 remote.PutSchemaData("", data, 0);
333 return 0;
336 static int DoApplyData(ISchemaBackend remote, string dir, VxCopyOpts opts)
338 log.print("Pushing data.\n");
340 if (!Directory.Exists(dir))
341 return 5; // nothing to do
343 string datadir = Path.Combine(dir, "DATA");
344 if (Directory.Exists(datadir))
345 dir = datadir;
347 var files =
348 from f in Directory.GetFiles(dir)
349 where !f.EndsWith("~")
350 select f;
351 return ApplyFiles(files.ToArray(), remote, opts);
354 public static void Main(string[] args)
356 // command line options
357 VxCopyOpts opts = VxCopyOpts.Verbose;
359 int verbose = (int)WvLog.L.Info;
360 var extra = new OptionSet()
361 .Add("dry-run", delegate(string v) { opts |= VxCopyOpts.DryRun; } )
362 .Add("f|force", delegate(string v) { opts |= VxCopyOpts.Destructive; } )
363 .Add("v|verbose", delegate(string v) { verbose++; } )
364 .Parse(args);
366 WvLog.maxlevel = (WvLog.L)verbose;
368 if (extra.Count != 3)
370 ShowHelp();
371 return;
374 string cmd = extra[0];
375 string moniker = extra[1];
376 string dir = extra[2];
378 log.print("Connecting to '{0}'\n", moniker);
379 ISchemaBackend remote = VxSchema.create(moniker);
381 if (cmd == "pull")
382 DoExport(remote, dir, opts);
383 else if (cmd == "push")
384 DoApply(remote, dir, opts);
385 else if (cmd == "dpull")
386 DoGetData(remote, dir, opts);
387 else if (cmd == "dpush")
388 DoApplyData(remote, dir, opts);
389 else
391 Console.Error.WriteLine("\nUnknown command '{0}'\n", cmd);
392 ShowHelp();