2 using System
.Collections
.Generic
;
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
);
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
22 --dry-run: lists the files that would be changed but doesn't modify them.
27 static int DoExport(ISchemaBackend remote
, string dir
, VxCopyOpts opts
)
29 log
.print("Pulling schema.\n");
30 VxDiskSchema disk
= new VxDiskSchema(dir
);
33 = VxSchema
.CopySchema(remote
, disk
, opts
| VxCopyOpts
.Verbose
);
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
);
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
);
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
);
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('#');
74 retval
= retval
.Remove(comment_pos
);
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
);
101 err
.print("Invalid command. ({0})\n", line
);
105 Command newcmd
= new Command();
107 newcmd
.pri
= wv
.atoi(parts
[0]);
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)
122 err
.print("Invalid 'export' syntax. " +
123 "('{0}' should be 'where' in '{1}')\n", w
, line
);
126 newcmd
.table
= parts
[2];
127 newcmd
.where
= parts
[4];
131 err
.print("Invalid 'export' syntax. ({0})", line
);
135 else if (newcmd
.cmd
== "zap")
137 if (parts
.Length
< 3)
139 err
.print("Syntax: 'zap <tables...>'. ({0})\n", line
);
142 string[] tables
= line
.Split(whitespace
, 3,
143 StringSplitOptions
.RemoveEmptyEntries
);
144 newcmd
.table
= tables
[2];
148 err
.print("Command '{0}' unknown. ({1})\n", newcmd
.cmd
, line
);
155 static IEnumerable
<Command
> ParseCommands(string[] command_strs
)
157 List
<Command
> commands
= new List
<Command
>();
159 foreach (string _line
in command_strs
)
161 string line
= FixLine(_line
);
162 if (line
.Length
== 0)
165 Command cmd
= ParseCommand(line
);
169 if (last_pri
>= cmd
.pri
)
171 err
.print("Priority code '{0}' <= previous '{1}'. ({2})\n",
172 cmd
.pri
, last_pri
, line
);
182 static int DoGetData(ISchemaBackend remote
, string dir
,
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
);
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
);
212 if (commands
== null)
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
);
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
))
241 data
.Append(wv
.fmt("DELETE FROM [{0}]\n\n", table
));
244 List
<string> todelete
= new List
<string>();
245 foreach (var p
in dbsums
)
248 VxSchema
.ParseKey(p
.Value
.key
, out type
, out name
);
253 foreach (string name
in todelete
)
254 data
.Append(wv
.fmt("DELETE FROM [{0}]\n", name
));
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
);
265 File
.WriteAllBytes(outname
, data
.ToString().ToUTF8());
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
);
281 int dashidx
= info
.Name
.IndexOf('-');
285 string pristr
= info
.Name
.Remove(dashidx
);
286 string rest
= info
.Name
.Substring(dashidx
+ 1);
287 int pri
= wv
.atoi(pristr
);
292 if (info
.Extension
.ToLower() == ".sql")
293 rest
= rest
.Remove(rest
.ToLower().LastIndexOf(".sql"));
303 static int ApplyFiles(string[] files
, ISchemaBackend remote
,
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")
318 log
.print("Applying data from {0}\n", file
);
319 if ((opts
& VxCopyOpts
.DryRun
) != 0)
324 remote
.PutSchemaData(tablename
, data
, seqnum
);
328 // File we didn't generate, try to apply it anyway.
329 remote
.PutSchemaData("", data
, 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
))
348 from f
in Directory
.GetFiles(dir
)
349 where
!f
.EndsWith("~")
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++; }
)
366 WvLog
.maxlevel
= (WvLog
.L
)verbose
;
368 if (extra
.Count
!= 3)
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
);
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
);
391 Console
.Error
.WriteLine("\nUnknown command '{0}'\n", cmd
);