Index: trunk/ippTools/src/Makefile.am
===================================================================
--- trunk/ippTools/src/Makefile.am	(revision 42806)
+++ trunk/ippTools/src/Makefile.am	(revision 42837)
@@ -24,4 +24,7 @@
 	stacktool \
 	staticskytool \
+	xcstacktool \
+	xcskytool \
+	xcfftool \
 	warptool \
 	receivetool \
@@ -82,4 +85,7 @@
 	regtool.h \
 	stacktool.h \
+	xcstacktool.h \
+	xcskytool.h \
+	xcfftool.h \
 	staticskytool.h \
 	warptool.h \
@@ -245,4 +251,22 @@
     staticskytoolConfig.c
 
+xcstacktool_CFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(IPPDB_CFLAGS)
+xcstacktool_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(IPPDB_LIBS) libpxtools.la
+xcstacktool_SOURCES = \
+    xcstacktool.c \
+    xcstacktoolConfig.c
+
+xcskytool_CFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(IPPDB_CFLAGS)
+xcskytool_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(IPPDB_LIBS) libpxtools.la
+xcskytool_SOURCES = \
+    xcskytool.c \
+    xcskytoolConfig.c
+
+xcfftool_CFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(IPPDB_CFLAGS)
+xcfftool_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(IPPDB_LIBS) libpxtools.la
+xcfftool_SOURCES = \
+    xcfftool.c \
+    xcfftoolConfig.c
+
 pxadmin_CFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(IPPDB_CFLAGS)
 pxadmin_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(IPPDB_LIBS) libpxtools.la
Index: trunk/ippTools/src/addtool.c
===================================================================
--- trunk/ippTools/src/addtool.c	(revision 42806)
+++ trunk/ippTools/src/addtool.c	(revision 42837)
@@ -1694,8 +1694,8 @@
   // required
   PXOPT_LOOKUP_STR(minidvodb_group, config->args, "-set_minidvodb_group", true, false);
-  PXOPT_LOOKUP_STR(minidvodb_path, config->args, "-set_minidvodb_path", false, false);
+  PXOPT_LOOKUP_STR(minidvodb_path_raw, config->args, "-set_minidvodb_path", false, false);
   PXOPT_LOOKUP_STR(minidvodb_host, config->args, "-set_minidvodb_host", false, false);
   //optional
-  PXOPT_LOOKUP_STR(minidvodb_name, config->args, "-set_minidvodb_name", false, false);
+  PXOPT_LOOKUP_STR(minidvodb_name_raw, config->args, "-set_minidvodb_name", false, false);
   PXOPT_LOOKUP_STR(state, config->args, "-set_state", false, false);
   PXOPT_LOOKUP_STR(note,            config->args, "-set_note",            false, false);
@@ -1706,19 +1706,9 @@
   }
 
-//  psString minidvodbpath = "NULL";
-
-  // I don't know how to get around the complaints of minidvodb_path can't be null. this 'fixes' it, but someone smarter can fix it properly.
-//  if (minidvodb_path) {
-//    minidvodbpath = minidvodb_path;
-//  } else {
-//    psError(PS_ERR_UNKNOWN, false, "require minidvodb_path");
-//    return false;
-//  }
-  
   if (!minidvodbRunInsert(config->dbh,
 			  0, // job_id
-			  minidvodb_name,
+			  minidvodb_name_raw,
 			  minidvodb_group,
-			  minidvodb_path,
+			  minidvodb_path_raw,
 			  minidvodb_host,
 			  "new",
@@ -1741,26 +1731,48 @@
   printf("%" PRId64 "\n", minidvodb_id);
 
-  
-  if (!minidvodb_name) {
-    psStringAppend(&minidvodb_name, "%s.%" PRIu64,minidvodb_group,minidvodb_id);
-  }
-  if (minidvodb_path) {
-    psStringAppend(&minidvodb_path,"/%s",minidvodb_name);
-  }
-
-  psString query = NULL;
-
-  psStringAppend(&query, "UPDATE minidvodbRun SET minidvodb_path = '%s', minidvodb_name = '%s' where minidvodb_id = %" PRIu64";", minidvodb_path, minidvodb_name, minidvodb_id);
-
-  if (!p_psDBRunQuery(config->dbh, query)) {
-    psError(PS_ERR_UNKNOWN, false, "database error");
-    psFree(query);
-    return false;
-  }
-
-  if (!psDBCommit(config->dbh)) {
-    psError(PS_ERR_UNKNOWN, false, "database error");
-    return false;
-  }
+  // we created a new entry using the values supplied above for minidvodb_name_raw, minidvodb_host_raw.
+  // If minidvodb_name is not specified, it will be generated based on the value of minidvodb_group and
+  // the minidvodb_id of the new entry.  But this means we need to insert first, get the ID, then update.
+
+  bool doUpdate = false;
+
+  // Below: note that minidvodb_name_raw and minidvodb_path_raw are stored on config->args
+  // we need to work with a temporary external variable to avoid changing entries on the MD
+
+  // if minidvodb_name_raw is not specified above, create it from minidvodb_group
+  psString minidvodb_name = NULL;
+  if (minidvodb_name_raw) {
+    minidvodb_name = psStringCopy (minidvodb_name_raw);
+  } else {
+    psStringAppend(&minidvodb_name, "%s.%" PRIu64, minidvodb_group, minidvodb_id);
+    doUpdate = true;
+  }
+
+  // if minidvodb_path_raw IS specified above, update it with the minidvodb_name
+  psString minidvodb_path = NULL;
+  if (minidvodb_path_raw) {
+    psStringAppend(&minidvodb_path, "%s/%s", minidvodb_path_raw, minidvodb_name);
+    doUpdate = true;
+  }
+
+  if (doUpdate) {
+    psString query = NULL;
+
+    psStringAppend(&query, "UPDATE minidvodbRun SET minidvodb_path = '%s', minidvodb_name = '%s' where minidvodb_id = %" PRIu64";", minidvodb_path, minidvodb_name, minidvodb_id);
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      psFree(query);
+      return false;
+    }
+
+    if (!psDBCommit(config->dbh)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      return false;
+    }
+  }
+
+  psFree (minidvodb_name);
+  psFree (minidvodb_path);
 
   return true;
Index: trunk/ippTools/src/disttool.c
===================================================================
--- trunk/ippTools/src/disttool.c	(revision 42806)
+++ trunk/ippTools/src/disttool.c	(revision 42837)
@@ -765,8 +765,6 @@
     }
 
-    if (simple) {/* no option? */}
-
     if (pretend) {
-        if (!ippdbPrintMetadatas(stdout, output, "distRunsToUpdate", true)) {
+        if (!ippdbPrintMetadatas(stdout, output, "distRunsToUpdate", !simple)) {
             psError(PS_ERR_UNKNOWN, false, "failed to print array");
             psFree(output);
@@ -1652,4 +1650,5 @@
     PXOPT_LOOKUP_STR(state, config->args, "-set_state", false, false);
     PXOPT_LOOKUP_STR(comment, config->args, "-comment", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
 
     distTargetRow *row = distTargetRowAlloc(
@@ -1676,5 +1675,5 @@
     row->target_id = psDBLastInsertID(config->dbh);
 
-    if (!distTargetPrintObject(stdout, row, true)) {
+    if (!distTargetPrintObject(stdout, row, !simple)) {
             psError(PS_ERR_UNKNOWN, false, "failed to print object");
             psFree(row);
@@ -1815,4 +1814,5 @@
     PXOPT_LOOKUP_STR(last_fileset, config->args, "-last_fileset", false, false);
     PXOPT_LOOKUP_STR(state, config->args,        "-set_state", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args,      "-simple", false);
 
     // XXX: should we insure that these names do not contatin any whitespace?
@@ -1842,5 +1842,5 @@
     row->dest_id = psDBLastInsertID(config->dbh);
 
-    if (!rcDestinationPrintObject(stdout, row, true)) {
+    if (!rcDestinationPrintObject(stdout, row, !simple)) {
         psError(PS_ERR_UNKNOWN, false, "failed to print object");
         psFree(row);
Index: trunk/ippTools/src/disttoolConfig.c
===================================================================
--- trunk/ippTools/src/disttoolConfig.c	(revision 42806)
+++ trunk/ippTools/src/disttoolConfig.c	(revision 42837)
@@ -275,4 +275,5 @@
     psMetadataAddStr(definedestinationArgs, PS_LIST_TAIL, "-last_fileset",0, "define last_fileset", NULL);
     psMetadataAddStr(definedestinationArgs, PS_LIST_TAIL, "-set_state",   0, "define state", NULL);
+    psMetadataAddBool(definedestinationArgs, PS_LIST_TAIL, "-simple",  0, "use the simple output format", false);
 
     // -updatedestination
@@ -292,4 +293,5 @@
     psMetadataAddStr(definetargetArgs, PS_LIST_TAIL, "-set_state", 0, "define state", NULL);
     psMetadataAddStr(definetargetArgs, PS_LIST_TAIL, "-comment",   0, "define comment", NULL);
+    psMetadataAddBool(definetargetArgs, PS_LIST_TAIL, "-simple",  0, "use the simple output format", false);
 
     // -updatetarget
Index: trunk/ippTools/src/fftool.c
===================================================================
--- trunk/ippTools/src/fftool.c	(revision 42806)
+++ trunk/ippTools/src/fftool.c	(revision 42837)
@@ -104,5 +104,8 @@
 }
 
-
+// NOTE: this mode seems to be insufficiently constrained.  It first identifies the
+// set of stackRun/skycalRun entries that lack a fullForceRun match.  But the
+// second stage is selecting all warps only based on (tess_id, skycell_id, filter) without
+// restriction to those which contributed to the stack
 static bool definebyqueryMode(pxConfig *config)
 {
@@ -150,4 +153,6 @@
     }
     PXOPT_COPY_F32(config->args, warpWhereMD, "-select_good_frac_min",  "warpSkyfile.good_frac",   ">=");
+    // XXX the tess_id and filter below can never be used because they are removed above (for stack)
+    // these options should be something like -select_warp_tess_id and -select_warp_filter
     PXOPT_COPY_STR(config->args, warpWhereMD, "-select_tess_id",       "warpRun.tess_id",         "==");
     pxAddLabelSearchArgs(config, warpWhereMD, "-select_filter",        "rawExp.filter",          "LIKE");
@@ -223,4 +228,5 @@
     psString warpQueryTemplate = pxDataGet("fftool_definebyquery_select_warps.sql");
 
+    // NOTE: these allow the set of input warps to be restricted
     whereClause = psDBGenerateWhereConditionSQL(warpWhereMD, NULL);
     psStringAppend(&warpQueryTemplate, "\nAND %s", whereClause);
@@ -339,4 +345,7 @@
 }
 
+// NOTE: this mode and 'definebyquery' seem to be nearly identical except that
+// 'definebyquery' allows for restrictions on the input warps by label, data_group,
+// warp_id, tess_id, filter, good_frac
 static bool defineforstacksMode(pxConfig *config)
 {
Index: trunk/ippTools/src/pztool.c
===================================================================
--- trunk/ippTools/src/pztool.c	(revision 42806)
+++ trunk/ippTools/src/pztool.c	(revision 42837)
@@ -50,6 +50,5 @@
 static bool updatenewexpMode(pxConfig *config);
 
-// XXX EAM : 2021.05.18 : code for updatesummitExp was added but not finished.
-// XXX static bool updatesummitExpMode(pxConfig *config);
+static bool updatesummitExpMode(pxConfig *config);
 
 // static bool copydoneCompleteExp(pxConfig *config);
@@ -93,5 +92,5 @@
 	MODECASE(PZTOOL_MODE_UPDATEPZEXP, updatepzexpMode);
         MODECASE(PZTOOL_MODE_UPDATENEWEXP, updatenewexpMode);
-	// XXX MODECASE(PZTOOL_MODE_UPDATESUMMITEXP, updatesummitExpMode);
+	MODECASE(PZTOOL_MODE_UPDATESUMMITEXP, updatesummitExpMode);
         default:
             psAbort("invalid option (this should not happen)");
@@ -810,5 +809,4 @@
 }
 
-
 static bool updatecopiedMode(pxConfig *config)
 {
@@ -939,6 +937,5 @@
 }
 
-# if (0)
-static bool summitExpSetFault(pxConfig *config, const psS64 summit_id, const int fault)
+static bool summitExpSetFault(pxConfig *config, const psString exp_name, const psS64 summit_id, const int fault)
 {
     // check that state is a valid string value
@@ -949,7 +946,7 @@
   }
 
-    char *query = "UPDATE summitExp SET fault = %d WHERE summit_id = %ld";
-    if (!p_psDBRunQueryF(config->dbh, query, fault, summit_id)) {
-        psError(PS_ERR_UNKNOWN, false, "failed to change state for %lld", (long long) summit_id);
+    char *query = "UPDATE summitExp SET fault = %d WHERE summit_id = %ld AND exp_name = '%s'";
+    if (!p_psDBRunQueryF(config->dbh, query, fault, summit_id, exp_name)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to change state for %s = %lld", exp_name, (long long) summit_id);
         return false;
     }
@@ -960,17 +957,16 @@
 static bool updatesummitExpMode(pxConfig *config)
 {
-  int fault;
-
+  // require both exp_name and summit_id as a double-check
   PS_ASSERT_PTR_NON_NULL(config, false);
   PXOPT_LOOKUP_S64(summit_id, config->args, "-summit_id", true, false);
-  PXOPT_LOOKUP_S32(state,    config->args, "-set_fault",true, false);
-
-  if (!summitExpSetFault(config,summit_id, fault)) {
-    psError(PS_ERR_UNKNOWN, false, "failed to change state for %lld", (long long) summit_id);
-    return false;
+  PXOPT_LOOKUP_STR(exp_name,  config->args, "-exp_name",  true, false);
+  PXOPT_LOOKUP_S32(fault,     config->args, "-set_fault", true, false);
+
+  if (!summitExpSetFault(config, exp_name, summit_id, fault)) {
+      psError(PS_ERR_UNKNOWN, false, "failed to change state for %s = %lld", exp_name, (long long) summit_id);
+      return false;
   }
   return true;
 }
-# endif  
   
 static bool updatepzexpMode(pxConfig *config)
Index: trunk/ippTools/src/pztoolConfig.c
===================================================================
--- trunk/ippTools/src/pztoolConfig.c	(revision 42806)
+++ trunk/ippTools/src/pztoolConfig.c	(revision 42837)
@@ -171,13 +171,9 @@
     psMetadataAddStr(updatepzexpArgs, PS_LIST_TAIL, "-set_state",  0,            "define new state (required)", NULL);
     
-# if (0)
     // -updatesummitexp
     psMetadata *updatesummitexpArgs = psMetadataAlloc();
     psMetadataAddS64(updatesummitexpArgs, PS_LIST_TAIL, "-summit_id", 0,     "define summit_id", 0);
     psMetadataAddStr(updatesummitexpArgs, PS_LIST_TAIL, "-exp_name",   0,            "search by exposure name (required)", NULL);
-    psMetadataAddStr(updatesummitexpArgs, PS_LIST_TAIL, "-inst",       0,            "search by camera (required)", NULL);
-    psMetadataAddStr(updatesummitexpArgs, PS_LIST_TAIL, "-telescope",  0,            "search by telescope (required)", NULL);
     psMetadataAddS32(updatesummitexpArgs, PS_LIST_TAIL, "-set_fault",  0,            "define new fault (required)", 0);
-# endif
     
     // -updatenewexp
Index: trunk/ippTools/src/stacktool.c
===================================================================
--- trunk/ippTools/src/stacktool.c	(revision 42806)
+++ trunk/ippTools/src/stacktool.c	(revision 42837)
@@ -496,7 +496,4 @@
             psFree(insert);
             psFree(list);
-            if (!psDBRollback(config->dbh)) {
-                psError(PS_ERR_UNKNOWN, false, "database error");
-            }
             return false;
         }
Index: trunk/ippTools/src/staticskytool.c
===================================================================
--- trunk/ippTools/src/staticskytool.c	(revision 42806)
+++ trunk/ippTools/src/staticskytool.c	(revision 42837)
@@ -1061,6 +1061,6 @@
     PXOPT_COPY_S64(config->args, whereMD, "-select_sky_id",        "staticskyRun.sky_id",       "==");
     PXOPT_COPY_S64(config->args, whereMD, "-select_stack_id",      "stackRun.stack_id",         "==");
-    PXOPT_COPY_STR(config->args, whereMD, "-select_skycell_id",    "stackRun.skycell_id",       "==");
     PXOPT_COPY_STR(config->args, whereMD, "-select_tess_id",       "stackRun.tess_id",          "==");
+    PXOPT_COPY_STR(config->args, whereMD, "-select_skycell_id",    "stackRun.skycell_id",       "LIKE");
     pxAddLabelSearchArgs(config, whereMD, "-select_filter",        "stackRun.filter",           "LIKE");
     PXOPT_COPY_F32(config->args, whereMD, "-select_good_frac_min", "stackSumSkyfile.good_frac", ">=");
Index: trunk/ippTools/src/xcfftool.c
===================================================================
--- trunk/ippTools/src/xcfftool.c	(revision 42837)
+++ trunk/ippTools/src/xcfftool.c	(revision 42837)
@@ -0,0 +1,1202 @@
+/*
+ * xcfftool.c
+ *
+ * Copyright (C) 2013 Institute for Astronomy, University of Hawaii
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * program; see the file COPYING. If not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVB_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+#include <ippdb.h>
+
+#include "pxtools.h"
+#include "pxspace.h"
+#include "xcfftool.h"
+
+static bool definebyqueryMode(pxConfig *config);
+static bool updaterunMode(pxConfig *config);
+static bool todoMode(pxConfig *config);
+static bool addresultMode(pxConfig *config);
+static bool resultMode(pxConfig *config);
+static bool revertMode(pxConfig *config);
+static bool updateresultMode(pxConfig *config);
+static bool toadvanceMode(pxConfig *config);
+static bool addsummaryMode(pxConfig *config);
+static bool revertsummaryMode(pxConfig *config);
+static bool updatesummaryMode(pxConfig *config);
+static bool summaryMode(pxConfig *config);
+static bool exportrunMode(pxConfig *config);
+static bool importrunMode(pxConfig *config);
+
+static bool setxcForceRunState(pxConfig *config, psS64 xcff_id, const char *state);
+static psS64 getCameraIDfromName(pxConfig *config, const char *dbname, const char *camera);
+
+# define MODECASE(caseName, func) \
+    case caseName: \
+    if (!func(config)) { \
+        goto FAIL; \
+    } \
+    break;
+
+int main(int argc, char **argv)
+{
+    psLibInit(NULL);
+
+    pxConfig *config = xcfftoolConfig(NULL, argc, argv);
+    if (!config) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to configure");
+        goto FAIL;
+    }
+
+    switch (config->mode) {
+        MODECASE(XCFFTOOL_MODE_DEFINEBYQUERY,     definebyqueryMode);
+        MODECASE(XCFFTOOL_MODE_UPDATERUN,         updaterunMode);
+        MODECASE(XCFFTOOL_MODE_TODO,              todoMode);
+        MODECASE(XCFFTOOL_MODE_ADDRESULT,         addresultMode);
+        MODECASE(XCFFTOOL_MODE_RESULT,            resultMode);
+        MODECASE(XCFFTOOL_MODE_REVERT,            revertMode);
+        MODECASE(XCFFTOOL_MODE_UPDATERESULT,      updateresultMode);
+        MODECASE(XCFFTOOL_MODE_TOADVANCE,         toadvanceMode);
+        MODECASE(XCFFTOOL_MODE_ADDSUMMARY,        addsummaryMode);
+        MODECASE(XCFFTOOL_MODE_REVERTSUMMARY,     revertsummaryMode);
+        MODECASE(XCFFTOOL_MODE_UPDATESUMMARY,     updatesummaryMode);
+        MODECASE(XCFFTOOL_MODE_SUMMARY,           summaryMode);
+        MODECASE(XCFFTOOL_MODE_EXPORTRUN,         exportrunMode);
+        MODECASE(XCFFTOOL_MODE_IMPORTRUN,         importrunMode);
+        default:
+            psAbort("invalid option (this should not happen)");
+    }
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(EXIT_SUCCESS);
+
+FAIL:
+    psErrorStackPrint(stderr, "\n");
+    int exit_status = pxerrorGetExitStatus();
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(exit_status);
+}
+
+// NOTE: if I denormalize and populate (tess_id, skycell_id) in xcForceRun then I do not need
+// to join back to xcstackRun to get that info
+static bool definebyqueryMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // I need the name of the current working database
+    psMetadataItem *dbnameMD = pmConfigUserSite(config->modules, "DBNAME", PS_DATA_STRING);
+    char *dbname = dbnameMD->data.str;
+
+    // required:
+    PXOPT_LOOKUP_STR(workdir,     config->args, "-set_workdir", true, false);
+    PXOPT_LOOKUP_STR(label,       config->args, "-set_label", true, false);
+
+    // we have two relevant databases: the current working database (e.g., gpc1) and the cross-camera
+    // database used to generate the cross-camera stacks (e.g., xcamera).  the cross-camera database
+    // name is supplied with -xcamera as an argument.  the current working database is part of the
+    // database config info
+
+    // The xcamera must be supplied as a string, then the camera_id is selected
+    // from the table xcstackCamera.  
+    PXOPT_LOOKUP_STR(xcamera, config->args, "-xcamera", true, false);
+
+    // get camera_id for the working database from the xcamera database
+    psS64 camera_id = getCameraIDfromName(config, xcamera, dbname);
+    if (!camera_id) {
+	psError(PXTOOLS_ERR_SYS, false, "failed to find camera %s in database", dbname);
+	return false;
+    }
+    // get xcamera_id for the cross-camera database from the working database
+    psS64 xcamera_id = getCameraIDfromName(config, dbname, xcamera);
+    if (!xcamera_id) {
+	psError(PXTOOLS_ERR_SYS, false, "failed to find camera %s in database", xcamera);
+	return false;
+    }
+
+    // optional
+    PXOPT_LOOKUP_STR(data_group,  config->args, "-set_data_group", false, false);
+    PXOPT_LOOKUP_STR(dist_group,  config->args, "-set_dist_group", false, false);
+    PXOPT_LOOKUP_STR(note,        config->args, "-set_note", false, false);
+    PXOPT_LOOKUP_STR(reduction,   config->args, "-set_reduction", false, false);
+
+    // first, we need to select the xccalRun(s) which need to have xcForceRuns.  we can limit the selection based on
+    // qualities of the xccalRun or the xcstackRun.  There will be one xcForceRun for each
+    // identified xccalRun (there may be multiple xcForceRuns with different labels)
+
+    psMetadata *xccalWhereMD = psMetadataAlloc();
+    pxAddLabelSearchArgs(config, xccalWhereMD, "-select_xccal_label",      "xccalRun.label",      "LIKE");
+    pxAddLabelSearchArgs(config, xccalWhereMD, "-select_xccal_data_group", "xccalRun.data_group", "LIKE");
+    PXOPT_COPY_S64(config->args, xccalWhereMD, "-select_xccal_id",         "xccalRun.xccal_id",   "==");
+
+    if (!psListLength(xccalWhereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "xccal search parameters are required");
+        psFree(xccalWhereMD);
+        return false;
+    }
+
+    // the select query does not join to xcstackRun, but it does join to stackRun
+    PXOPT_COPY_STR(config->args, xccalWhereMD, "-select_skycell_id",    "stackRun.skycell_id",      "LIKE");
+    PXOPT_COPY_STR(config->args, xccalWhereMD, "-select_tess_id",       "stackRun.tess_id",         "==");
+    pxAddLabelSearchArgs(config, xccalWhereMD, "-select_filter",        "stackRun.filter",          "LIKE");
+    if (!pxskycellAddWhere(config, xccalWhereMD)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        psFree(xccalWhereMD);
+        return false;
+    }
+
+    // once we have identified the xccalRun(s) for which we will generate the xcForceRuns, we
+    // need to select the input warps.  Normally, these would be all warps used to generate all
+    // stacks used to generate the xcstack skyfile associated with the xccalRun.  But we have
+    // the options to limit which warps we actually include.  
+
+    psMetadata *warpWhereMD = psMetadataAlloc();
+    pxAddLabelSearchArgs(config, warpWhereMD, "-select_warp_label",      "warpRun.label",           "LIKE");
+    pxAddLabelSearchArgs(config, warpWhereMD, "-select_warp_data_group", "warpRun.data_group",      "LIKE");
+    PXOPT_COPY_S64(config->args, warpWhereMD, "-select_warp_id",         "warpRun.warp_id",         "==");
+    PXOPT_COPY_F32(config->args, warpWhereMD, "-select_good_frac_min",   "warpSkyfile.good_frac",  ">=");
+    // it is necessary to limit the warps otherwise the warpQueryTemplate (line 282) will be ill-formed
+    if (!psListLength(warpWhereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "warp search parameters are required");
+        psFree(warpWhereMD);
+        psFree(xccalWhereMD);
+        return false;
+    }
+
+    PXOPT_LOOKUP_BOOL(rerun, config->args, "-rerun", false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+    PXOPT_LOOKUP_BOOL(pretend, config->args, "-pretend", false);
+
+    psString select = NULL;
+
+    psString query_pt1 = pxDataGet("xcfftool_definebyquery_pt1.sql");
+    if (!query_pt1) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        psFree(xccalWhereMD);
+        psFree(warpWhereMD);
+        return false;
+    }
+
+    // query_pt1 has 4 places where the xcamera database name must be supplied
+    psStringAppend (&select, query_pt1, xcamera, xcamera, xcamera, xcamera);
+    psFree (query_pt1);
+
+    if (!rerun) {
+	psStringAppend(&select, "LEFT JOIN xcForceRun ON (xcForceRun.xccal_id = %s.xccalRun.xccal_id) AND (xcForceRun.label = '%s')\n", xcamera, label);
+    }
+
+    psString query_pt2 = pxDataGet("xcfftool_definebyquery_pt2.sql");
+    if (!query_pt2) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        psFree(xccalWhereMD);
+        psFree(warpWhereMD);
+        psFree(select);
+        return false;
+    }
+
+    // query_pt2 has 3 places where the xcamera database name must be supplied and 1 for this camera
+    psStringAppend (&select, query_pt2, xcamera, (long long) camera_id, xcamera, xcamera);
+    psFree (query_pt2);
+
+    psString where = NULL;
+    psString whereClause = psDBGenerateWhereConditionSQL(xccalWhereMD, NULL);
+    psStringAppend(&select, "\n AND %s", whereClause);
+
+    psFree(whereClause);
+    psFree(xccalWhereMD);
+
+    // accept only the entries without a match in the target label
+    if (!rerun) psStringAppend(&select, "\n AND xcff_id IS NULL\n");
+
+    if (!p_psDBRunQuery(config->dbh, select)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(select);
+        return false;
+    }
+    psFree(select);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        psFree(where);
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psWarning("xcfftool: no rows found");
+        psFree(output);
+        psFree(where);
+        return true;
+    }
+
+    if (pretend) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "toFullForce", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            psFree(where);
+            return false;
+        }
+        psFree(output);
+        psFree(where);
+        return true;
+    }
+
+    // this query selects all of the warps where were inputs to the given stackRun,
+    // with restrictions based on (quality, fault, warpRun.state, and other supplied conditions)
+    // there is no limit to the version of the warp analyss, to
+    psString warpQueryTemplate = pxDataGet("xcfftool_definebyquery_select_warps.sql");
+
+    // note the GROUP BY: if warpWhereMD is empty, this contruction
+    // will yield nonsense
+    whereClause = psDBGenerateWhereConditionSQL(warpWhereMD, NULL);
+    psStringAppend(&warpQueryTemplate, "\n AND %s GROUP BY warp_id", whereClause);
+
+    for (long i = 0; i < output->n; i++) {
+        psMetadata *row = output->data[i]; // Row from select
+        bool status;
+
+	if (!psDBTransaction(config->dbh)) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+            psFree(output);
+	    return false;
+	}
+
+        // psS64 warp_id = psMetadataLookupS64(&status, row, "warp_id");
+        psS64 xccal_id = psMetadataLookupS64(&status, row, "xccal_id");
+        psS64 stack_id = psMetadataLookupS64(&status, row, "stack_id");
+        psString path_base = psMetadataLookupStr(&status, row, "path_base");
+        psString xccal_data_group = psMetadataLookupStr(&status, row, "data_group");
+        psString tess_id = psMetadataLookupStr(&status, row, "tess_id");
+        psString skycell_id = psMetadataLookupStr(&status, row, "skycell_id");
+
+        psString query = NULL;
+        psStringAppend(&query, warpQueryTemplate, (long long) stack_id);
+
+        if (!p_psDBRunQuery(config->dbh, query)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+            psFree(query);
+            return false;
+        }
+        psFree(query);
+
+        // Find the warps for this skycell and filter combination
+        psArray *warpOutput = p_psDBFetchResult(config->dbh);
+        if (!warpOutput) {
+            psErrorCode err = psErrorCodeLast();
+            switch (err) {
+                case PS_ERR_DB_CLIENT:
+                    psError(PXTOOLS_ERR_SYS, false, "database error");
+                case PS_ERR_DB_SERVER:
+                    psError(PXTOOLS_ERR_PROG, false, "database error");
+                default:
+                    psError(PXTOOLS_ERR_PROG, false, "unknown error");
+            }
+            return false;
+        }
+        if (!psArrayLength(warpOutput)) {
+            // no warps for this xccal. Suprise?
+            psFree(warpOutput);
+            psFree(query);
+            continue;
+        }
+
+	// create an xcForceRun
+	if (!xcForceRunInsert(config->dbh,
+			      0x0,	     // xcff_id
+			      xccal_id,
+			      path_base,
+			      "new",	     // state
+			      workdir,
+			      label,
+			      skycell_id,
+			      tess_id,
+			      data_group ? data_group : (xccal_data_group ? xccal_data_group : label),
+			      dist_group,
+			      note,
+			      reduction,
+			      NULL        // registered
+		)
+	    ) {
+	    if (!psDBRollback(config->dbh)) {
+		psError(PS_ERR_UNKNOWN, false, "database error failed to rollback transaction");
+	    }
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+            psFree(output);
+	    return false;
+	}
+
+        psS64 xcff_id = psDBLastInsertID(config->dbh);
+
+        for (int j = 0; j < warpOutput->n; j++) {
+            psMetadata *warpRow = warpOutput->data[j];
+            psS64 warp_id = psMetadataLookupS64(&status, warpRow, "warp_id");
+
+            if (!xcForceInputInsert(config->dbh, xcff_id, warp_id, xcamera_id)) {
+                if (!psDBRollback(config->dbh)) {
+                    psError(PS_ERR_UNKNOWN, false, "database error failed to rollback transaction");
+                }
+                psError(PS_ERR_UNKNOWN, false, "database error");
+                psFree(warpOutput);
+                psFree(output);
+                return false;
+            }
+        }
+
+	if (!psDBCommit(config->dbh)) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+                psFree(warpOutput);
+	    psFree(output);
+	    return false;
+	}
+        psFree(warpOutput);
+    }
+    psFree(output);
+
+    return true;
+}
+
+static bool updaterunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id",     "xcff_id",    "==");
+    PXOPT_COPY_STR(config->args, where, "-label",       "label",      "==");
+    PXOPT_COPY_STR(config->args, where, "-state",       "state",      "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group",  "data_group", "==");
+    PXOPT_COPY_STR(config->args, where, "-dist_group",  "dist_group", "==");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id",  "skycell_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",     "tess_id",    "==");
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    psString query = psStringCopy ("UPDATE xcForceRun JOIN skycell USING(tess_id, skycell_id)");
+
+    // pxUpdateRun gets parameters from config->args and updates
+    bool result = pxUpdateRun(config, where, &query, "xcForceRun", "xcff_id", "xcForceResult", true, false);
+    psFree(query);
+    psFree(where);
+
+    return result;
+}
+
+
+static bool todoMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *whereMD = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, whereMD,  "-xcff_id", "xcff_id", "==");
+    pxAddLabelSearchArgs (config, whereMD, "-label",   "xcForceRun.label", "==");
+    pxskycellAddWhere(config, whereMD);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcfftool_todo.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (!psListLength(whereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        psFree(whereMD);
+        return false;
+    }
+
+    psString whereClause = psDBGenerateWhereConditionSQL(whereMD, NULL);
+    psStringAppend(&query, "\n AND %s", whereClause);
+    psFree(whereClause);
+    psFree(whereMD);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcfftool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xcForceRun", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool addresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    // required
+    PXOPT_LOOKUP_S64(xcff_id, config->args, "-xcff_id", true, false);
+    PXOPT_LOOKUP_S64(warp_id, config->args, "-warp_id", true, false);
+
+    // optional
+    PXOPT_LOOKUP_STR(path_base, config->args, "-path_base", false, false);
+    PXOPT_LOOKUP_STR(hostname, config->args, "-hostname", true, false);
+    PXOPT_LOOKUP_F32(dtime_script, config->args, "-dtime_script", false, false);
+    PXOPT_LOOKUP_STR(software_ver, config->args, "-software_ver", false, false);
+
+    // default values
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", false, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-quality", false, false);
+
+    // XXX not sure we need a transaction here...
+    if (!xcForceResultInsert(config->dbh,
+			       xcff_id,
+                               warp_id,
+                               path_base,
+                               dtime_script,
+                               quality,
+                               hostname,
+                               software_ver,
+                               fault
+          )) {
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+        }
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+
+static bool resultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id",    "xcForceRun.xcff_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-xccal_id",   "xcForceRun.xccal_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-warp_id",    "xcForceResult.warp_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-exp_id",     "rawExp.exp_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-exp_name",   "rawExp.exp_name", "==");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",    "xcForceRun.tess_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id", "xcForceRun.skycell_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-label",      "xcForceRun.label", "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group", "xcForceRun.data_group", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-filter",     "rawExp.filter", "LIKE");
+    PXOPT_COPY_S16(config->args, where, "-fault",      "xcForceResult.fault", "==");
+    PXOPT_COPY_S16(config->args, where, "-quality",    "xcForceResult.quality", "==");
+    pxskycellAddWhere(config, where);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcfftool_result.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " WHERE %s", whereClause);
+        psFree(whereClause);
+    } else {
+        psError(PXTOOLS_ERR_CONFIG, true, "search parameters or -all are required");
+        return false;
+    }
+
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, "\n%s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcfftool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        if (!ippdbPrintMetadatas(stdout, output, "xcForceResult", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+
+    return true;
+}
+
+static bool revertMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id", "xcForceRun.xcff_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-warp_id", "xcForceResult.warp_id", "==");
+    pxAddLabelSearchArgs(config, where, "-label", "xcForceRun.label", "==");
+    PXOPT_COPY_S16(config->args, where, "-fault", "xcForceResult.fault", "==");
+
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    // Delete product
+    psString delete = pxDataGet("xcfftool_revert.sql");
+    if (!delete) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&delete, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, delete)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(delete);
+        psFree(where);
+        return false;
+    }
+    psFree(delete);
+
+    int numRows = psDBAffectedRows(config->dbh); // Number of row affected
+    psLogMsg("xcfftool", PS_LOG_INFO, "Deleted %d rows", numRows);
+
+    psFree(where);
+    return true;
+}
+
+static bool updateresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", true, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-quality", false, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id", "xcff_id",   "==");
+    PXOPT_COPY_S64(config->args, where, "-warp_id", "warp_id",   "==");
+
+    if (!pxSetFaultCode(config->dbh, "xcForceResult", where, fault, quality)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to set set fault flag");
+        psFree (where);
+        return false;
+    }
+    psFree (where);
+    return true;
+}
+
+static bool toadvanceMode(pxConfig *config) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *whereMD = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, whereMD,  "-xcff_id", "xcff_id", "==");
+    pxAddLabelSearchArgs (config, whereMD, "-label", "xcForceRun.label", "==");
+    pxskycellAddWhere(config, whereMD);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcfftool_toadvance.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (!psListLength(whereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        psFree(whereMD);
+        return false;
+    }
+
+    psString whereClause = NULL;
+    psString temp = psDBGenerateWhereConditionSQL(whereMD, NULL);
+    psStringAppend(&whereClause, "\n AND %s", temp);
+    psFree(temp);
+    psFree(whereMD);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    // the where clause is required and is added to the query by the "WHERE hook" format string
+    if (!p_psDBRunQueryF(config->dbh, query, whereClause)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(whereClause);
+        psFree(query);
+        return false;
+    }
+    psFree(whereClause);
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcfftool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xcForceRun", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool addsummaryMode(pxConfig *config) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    // required
+    PXOPT_LOOKUP_S64(xcff_id, config->args, "-xcff_id", true, false);
+
+    // optional
+    PXOPT_LOOKUP_STR(path_base, config->args, "-path_base", false, false);
+    PXOPT_LOOKUP_STR(hostname, config->args, "-hostname", true, false);
+    PXOPT_LOOKUP_F32(dtime_script, config->args, "-dtime_script", false, false);
+    PXOPT_LOOKUP_STR(software_ver, config->args, "-software_ver", false, false);
+
+    // default values
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", false, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-quality", false, false);
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (!xcForceSummaryInsert(config->dbh,
+			       xcff_id,
+                               path_base,
+                               dtime_script,
+                               quality,
+                               hostname,
+                               software_ver,
+                               fault
+          )) {
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+        }
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (!fault) {
+        if (!setxcForceRunState(config, xcff_id, "full")) {
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            psError(PS_ERR_UNKNOWN, false, "failed to change staticskyRun state");
+            return false;
+        }
+    }
+
+    // point of no return
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+
+static bool summaryMode(pxConfig *config) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id",    "xcForceRun.xcff_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-xccal_id",   "xcForceRun.xccal_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-label",      "xcForceRun.label", "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group", "xcForceRun.data_group", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",    "xcForceRun.tess_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id", "xcForceRun.skycell_id", "LIKE");
+    PXOPT_COPY_S16(config->args, where, "-fault",      "xcForceSummary.fault", "==");
+    pxskycellAddWhere(config, where);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcfftool_summary.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " WHERE %s", whereClause);
+        psFree(whereClause);
+    } else {
+        psError(PXTOOLS_ERR_CONFIG, true, "search parameters or -all are required");
+        return false;
+    }
+
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, "\n%s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcfftool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        if (!ippdbPrintMetadatas(stdout, output, "xcForceSummary", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+
+    return true;
+}
+
+static bool revertsummaryMode(pxConfig *config) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id", "xcForceRun.xcff_id", "==");
+    pxAddLabelSearchArgs(config, where, "-label",   "xcForceRun.label", "==");
+    PXOPT_COPY_S16(config->args, where, "-fault",   "xcForceSummary.fault", "==");
+
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    // Delete product
+    psString delete = pxDataGet("xcfftool_revertsummary.sql");
+    if (!delete) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&delete, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, delete)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(delete);
+        psFree(where);
+        return false;
+    }
+    psFree(delete);
+
+    int numRows = psDBAffectedRows(config->dbh); // Number of row affected
+    psLogMsg("xcfftool", PS_LOG_INFO, "Deleted %d rows", numRows);
+
+    psFree(where);
+    return true;
+}
+
+static bool updatesummaryMode(pxConfig *config) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", true, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-quality", false, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id",   "xcff_id",   "==");
+
+    if (!pxSetFaultCode(config->dbh, "xcForceSummary", where, fault, quality)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to set set fault flag");
+        psFree (where);
+        return false;
+    }
+    psFree (where);
+    return true;
+}
+
+bool exportrunMode(pxConfig *config)
+{
+    typedef struct ExportTable {
+	char tableName[80];
+	char tableSQL[80];
+    } ExportTable;
+
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_STR(outfile, config->args, "-outfile", true,  false);
+    PXOPT_LOOKUP_U64(limit,   config->args, "-limit",   false, false);
+
+    FILE *f = fopen (outfile, "w");
+    if (f == NULL) {
+        psError(PS_ERR_UNKNOWN, false, "failed to open output file");
+        return false;
+    }
+
+    if (!pxExportVersion(config, f)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to write dbversion output file");
+        return false;
+    }
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcff_id", "xcff_id", "==");
+
+    ExportTable tables [] = {
+	{"xcForceRun",     "SELECT xcForceRun.*       FROM xcForceRun"},
+	{"xcForceInput",   "SELECT xcForceInput.*     FROM xcForceInput"},
+	{"xcForceResult",  "SELECT xcForceResult.*    FROM xcForceResult"},
+	{"xcForceSummary", "SELECT xcForceSummary.*   FROM xcForceSummary"},
+    };
+
+    int numTables = sizeof(tables)/sizeof(tables[0]);
+
+    for (int i=0; i < numTables; i++) {
+	psString query = psStringCopy(tables[i].tableSQL);
+
+	if (where && psListLength(where->list)) {
+	    psString whereClause = psDBGenerateWhereSQL(where, NULL);
+	    psStringAppend(&query, " %s", whereClause);
+	    psFree(whereClause);
+	}
+
+	// treat limit == 0 as "no limit"
+	if (limit) {
+	    psString limitString = psDBGenerateLimitSQL(limit);
+	    psStringAppend(&query, " %s", limitString);
+	    psFree(limitString);
+	}
+
+	if (!p_psDBRunQuery(config->dbh, query)) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    psFree(query);
+	    return false;
+	}
+	psFree(query);
+
+	psArray *output = p_psDBFetchResult(config->dbh);
+	if (!output) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    return false;
+	}
+	if (!psArrayLength(output)) {
+	    psError(PS_ERR_UNKNOWN, true, "no rows found");
+	    psFree(output);
+	    return false;
+	}
+
+	// we must write the export table in non-simple (true) format
+	if (!ippdbPrintMetadatas(f, output, tables[i].tableName, true)) {
+	    psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	    psFree(output);
+	    return false;
+	}
+	psFree(output);
+    }
+
+    fclose (f);
+
+    return true;
+}
+
+// XXX this is coded in a very strange way
+bool importrunMode(pxConfig *config)
+{
+  unsigned int nFail;
+
+  int numImportTables = 3;
+
+  char tables[3] [80] = {"xcForceInput", "xcForceResult", "xcForceSummary"};
+
+  PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+  PXOPT_LOOKUP_STR(infile, config->args, "-infile", true,  false);
+
+  psMetadata *input = psMetadataConfigRead (NULL, &nFail, infile, false);
+
+  if (!pxCheckImportVersion(config, input)) {
+      psError(PS_ERR_UNKNOWN, false, "pxCheckImportVersion failed");
+      return false;
+  }
+
+  psMetadataItem *item = psMetadataLookup (input, "xcForceRun");
+  psAssert (item, "entry not in input?");
+  psAssert (item->type == PS_DATA_METADATA_MULTI, "entry not multi?");
+
+  psMetadataItem *entry = psListGet (item->data.list, 0);
+  assert (entry);
+  assert (entry->type == PS_DATA_METADATA);
+  xcForceRunRow *xcForceRun = xcForceRunObjectFromMetadata (entry->data.md);
+  xcForceRunInsertObject (config->dbh, xcForceRun);
+
+  // fprintf (stdout, "---- warp run ----\n");
+  // psMetadataPrint (stderr, entry->data.md, 1);
+
+  for (int i = 0; i < numImportTables; i++) {
+    item = psMetadataLookup (input, tables[i]);
+    psAssert (item, "entry not in input?");
+    psAssert (item->type == PS_DATA_METADATA_MULTI, "entry not multi?");
+
+    switch (i) {
+      case 0:
+        for (int i = 0; i < item->data.list->n; i++) {
+          entry = psListGet (item->data.list, i);
+          assert (entry);
+          assert (entry->type == PS_DATA_METADATA);
+          xcForceInputRow *xcForceInput = xcForceInputObjectFromMetadata (entry->data.md);
+          xcForceInputInsertObject (config->dbh, xcForceInput);
+
+          // fprintf (stdout, "---- row %d ----\n", i);
+          // psMetadataPrint (stderr, entry->data.md, 1);
+        }
+        break;
+
+      case 1:
+        for (int i = 0; i < item->data.list->n; i++) {
+          entry = psListGet (item->data.list, i);
+          assert (entry);
+          assert (entry->type == PS_DATA_METADATA);
+          xcForceResultRow *xcForceResult = xcForceResultObjectFromMetadata (entry->data.md);
+          xcForceResultInsertObject (config->dbh, xcForceResult);
+
+          // fprintf (stdout, "---- row %d ----\n", i);
+          // psMetadataPrint (stderr, entry->data.md, 1);
+        }
+        break;
+
+      case 2:
+        for (int i = 0; i < item->data.list->n; i++) {
+          entry = psListGet (item->data.list, i);
+          assert (entry);
+          assert (entry->type == PS_DATA_METADATA);
+          xcForceSummaryRow *xcForceSummary = xcForceSummaryObjectFromMetadata (entry->data.md);
+          xcForceSummaryInsertObject (config->dbh, xcForceSummary);
+
+          // fprintf (stdout, "---- row %d ----\n", i);
+          // psMetadataPrint (stderr, entry->data.md, 1);
+        }
+        break;
+    }
+  }
+  return true;
+}
+
+// This function returns the camera_id associated with the camera name.
+// We expect the camera to be defined, so an invalid name or a missing camera
+// results in a 0 return value which should be treated as an error.
+static psS64 getCameraIDfromName(pxConfig *config, const char *dbname, const char *camera)
+{
+    PS_ASSERT_PTR_NON_NULL(camera, false);
+
+    char *query = "SELECT camera_id FROM %s.xcstackCamera WHERE camera = '%s'";
+    if (!p_psDBRunQueryF(config->dbh, query, dbname, camera)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to select camera_id for camera %s from database %s", camera, dbname);
+        return 0;
+    }
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        return false;
+    }
+
+    // we expect a value camera_id as a result
+    if (!psArrayLength(output)) {
+        psWarning("xcstacktool: no rows found");
+        psFree(output);
+        return false;
+    }
+
+    // we expect a single camera_id as a result
+    if (psArrayLength(output) > 1) {
+        psWarning("xcstacktool: too many camera?");
+        psFree(output);
+        return false;
+    }
+
+    // the query above selects stacks to be added to the xcstackRun
+    // for each stack, we need camera_id, stack_id, skycell_id, tess_id
+    psMetadata *row = output->data[0]; // Row from select
+    bool status;
+
+    // pull out the skycell_id, tess_id, filter
+    psS64 camera_id = psMetadataLookupS64(&status, row, "camera_id");
+    if (!status) {
+        psAbort("xcstacktool: camera_id not found in query result?");
+        psFree(output);
+        return false;
+    }
+
+    return camera_id;
+}
+
+static bool setxcForceRunState(pxConfig *config, psS64 xcff_id, const char *state)
+{
+    psString query = "UPDATE xcForceRun SET state = 'full' WHERE xcff_id = %" PRId64;
+    if (!p_psDBRunQueryF(config->dbh, query, xcff_id)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+
Index: trunk/ippTools/src/xcfftool.h
===================================================================
--- trunk/ippTools/src/xcfftool.h	(revision 42837)
+++ trunk/ippTools/src/xcfftool.h	(revision 42837)
@@ -0,0 +1,45 @@
+/*
+ * xcfftool.h
+ *
+ * Copyright (C) 2013 IfA Hawaii
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * program; see the file COPYING. If not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef XCFFTOOL_H
+#define XCFFTOOL_H 1
+
+#include "pxtools.h"
+
+typedef enum {
+    XCFFTOOL_MODE_NONE           = 0x0,
+    XCFFTOOL_MODE_DEFINEBYQUERY,
+    XCFFTOOL_MODE_UPDATERUN,
+    XCFFTOOL_MODE_TODO,
+    XCFFTOOL_MODE_ADDRESULT,
+    XCFFTOOL_MODE_RESULT,
+    XCFFTOOL_MODE_REVERT,
+    XCFFTOOL_MODE_UPDATERESULT,
+    XCFFTOOL_MODE_TOADVANCE,
+    XCFFTOOL_MODE_ADDSUMMARY,
+    XCFFTOOL_MODE_REVERTSUMMARY,
+    XCFFTOOL_MODE_UPDATESUMMARY,
+    XCFFTOOL_MODE_SUMMARY,
+    XCFFTOOL_MODE_EXPORTRUN,
+    XCFFTOOL_MODE_IMPORTRUN,
+} xcfftoolMode;
+
+pxConfig *xcfftoolConfig(pxConfig *config, int argc, char **argv);
+
+#endif // XCFFTOOL_H
Index: trunk/ippTools/src/xcfftoolConfig.c
===================================================================
--- trunk/ippTools/src/xcfftoolConfig.c	(revision 42837)
+++ trunk/ippTools/src/xcfftoolConfig.c	(revision 42837)
@@ -0,0 +1,245 @@
+/*
+ * xcfftool.c
+ *
+ * Copyright (C) 2013 Institute for Astronomy, University of Hawaii
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * program; see the file COPYING. If not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <psmodules.h>
+
+#include "pxtools.h"
+#include "xcfftool.h"
+
+pxConfig *xcfftoolConfig(pxConfig *config, int argc, char **argv)
+{
+    if (!config) {
+        config = pxConfigAlloc();
+    }
+
+    pmConfigReadParamsSet(false);
+
+    // setup site config
+    config->modules = pmConfigRead(&argc, argv, NULL);
+    if (!config->modules) {
+        psError(psErrorCodeLast(), false, "Can't find site configuration");
+        psFree(config);
+        return NULL;
+    }
+
+    psTime *now = psTimeGetNow(PS_TIME_TAI);
+    (void) now;
+
+    // -definebyquery
+    psMetadata *definebyqueryArgs = psMetadataAlloc();
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-xcamera", 0, "specify the cross-camera database", false);
+
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_xccal_label", PS_META_DUPLICATE_OK, "search by xccal label (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_xccal_data_group", PS_META_DUPLICATE_OK, "search by xccal data_group (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddS64(definebyqueryArgs,  PS_LIST_TAIL, "-select_xccal_id", 0, "search by xccal ID", 0);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_warp_label", PS_META_DUPLICATE_OK, "limits warps by label (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_warp_data_group", PS_META_DUPLICATE_OK, "limit warps by data_group (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddS64(definebyqueryArgs,  PS_LIST_TAIL, "-select_warp_id", 0, "limit warps by warp ID", 0);
+    psMetadataAddF32(definebyqueryArgs,  PS_LIST_TAIL, "-select_good_frac_min", 0, "mimimum good_frac in warp", 0.0);
+
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_skycell_id", 0, "limit by skycell_id (LIKE comparision)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_tess_id", 0, "limit by tess_id", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_filter", PS_META_DUPLICATE_OK, "limit by stack filter (LIKE comparison, multiple OK)", NULL);
+
+    pxskycellAddArguments(definebyqueryArgs);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_workdir", 0, "define workdir (required)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_label", 0, "define label (required)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_data_group", 0, "define data group", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_dist_group", 0, "define dist group", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_note", 0, "define note", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_reduction", 0, "define reduction", NULL);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-rerun",  0, "queue new run even if one already exists for inputs", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-pretend",  0, "do not actually modify the database", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -updaterun
+    psMetadata *updaterunArgs = psMetadataAlloc();
+    psMetadataAddS64(updaterunArgs, PS_LIST_TAIL, "-xcff_id",    0, "search by xcff ID", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-label",      0, "search by label", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-state",      0, "search by state", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-data_group", 0, "search by data_group", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-dist_group", 0, "search by dist_group", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-skycell_id", 0, "search by skycell_id", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-tess_id",    0, "search by tess_id", NULL);
+    pxskycellAddArguments(updaterunArgs);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_label", 0, "define new value for label", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_state", 0, "define new state", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_data_group", 0, "define new data_group", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_dist_group", 0, "define new dist_group", NULL);
+    psMetadataAddStr(updaterunArgs,  PS_LIST_TAIL, "-set_note", 0, "define note", NULL);
+
+    // -todo
+    psMetadata *todoArgs = psMetadataAlloc();
+    psMetadataAddS64(todoArgs, PS_LIST_TAIL, "-xcff_id", 0, "search by xcff ID", 0);
+    psMetadataAddStr(todoArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", NULL);
+    pxskycellAddArguments(todoArgs);
+    psMetadataAddU64(todoArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(todoArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -addresult
+    psMetadata *addresultArgs = psMetadataAlloc();
+    psMetadataAddS64(addresultArgs, PS_LIST_TAIL, "-xcff_id", 0, "define xcff ID (required)", 0);
+    psMetadataAddS64(addresultArgs, PS_LIST_TAIL, "-warp_id", 0, "define warp ID (required)", 0);
+    psMetadataAddStr(addresultArgs, PS_LIST_TAIL, "-path_base", 0, "define base output location(required)", 0);
+    psMetadataAddF32(addresultArgs, PS_LIST_TAIL, "-dtime_script", 0, "define elapsed time in script (seconds)", NAN);
+    psMetadataAddS16(addresultArgs, PS_LIST_TAIL, "-quality", 0, "set quality", 0);
+    psMetadataAddStr(addresultArgs, PS_LIST_TAIL, "-hostname", 0, "define hostname", 0);
+    psMetadataAddStr(addresultArgs, PS_LIST_TAIL, "-software_ver", 0, "define software version", 0);
+    psMetadataAddS16(addresultArgs, PS_LIST_TAIL, "-fault", 0, "set fault code", 0);
+
+    // -result
+    psMetadata *resultArgs= psMetadataAlloc();
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-xcff_id", 0, "search by xcforce ID", 0);
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-xccal_id", 0, "search by xccal ID", 0);
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-warp_id", 0, "search by warp ID", 0);
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-exp_id", 0, "search by exposure ID", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-exp_name", 0, "search by exposure name", 0);
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-skycal_id", 0, "search by skycal ID", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-tess_id", 0, "search by tess ID (LIKE comparison)", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-skycell_id", 0, "search by skycell ID (LIKE comparison)", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-label", 0, "search by label", NULL);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-filter", 0, "search by rawExp filter", NULL);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-data_group", 0, "search by data_group (LIKE comparison)", NULL);
+    pxskycellAddArguments(resultArgs);
+    psMetadataAddS16(resultArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    psMetadataAddS16(resultArgs, PS_LIST_TAIL, "-quality", 0, "search by quality value", 0);
+    psMetadataAddU64(resultArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(resultArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -revert
+    psMetadata *revertArgs= psMetadataAlloc();
+    psMetadataAddS64(revertArgs, PS_LIST_TAIL, "-xcff_id", 0, "search by xcff ID", 0);
+    psMetadataAddS64(revertArgs, PS_LIST_TAIL, "-warp_id", 0, "search by warp ID", 0);
+    psMetadataAddStr(revertArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", 0);
+    psMetadataAddS16(revertArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    pxskycellAddArguments(revertArgs);
+
+    // -updateresult
+    psMetadata *updateresultArgs = psMetadataAlloc();
+    psMetadataAddS64(updateresultArgs, PS_LIST_TAIL, "-xcff_id", 0, "define xcff ID (required)", 0);
+    psMetadataAddS64(updateresultArgs, PS_LIST_TAIL, "-warp_id", 0, "define warp ID (required)", 0);
+    psMetadataAddS16(updateresultArgs, PS_LIST_TAIL, "-fault", 0, "set fault code (required)", 0);
+    psMetadataAddS16(updateresultArgs, PS_LIST_TAIL, "-quality", 0, "set quality code", 0);
+
+    // -toadvance
+    psMetadata *toadvanceArgs = psMetadataAlloc();
+    psMetadataAddS64(toadvanceArgs, PS_LIST_TAIL, "-xcff_id", 0, "search by xcff ID", 0);
+    psMetadataAddStr(toadvanceArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", NULL);
+    pxskycellAddArguments(toadvanceArgs);
+    psMetadataAddU64(toadvanceArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(toadvanceArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -addsummary
+    psMetadata *addsummaryArgs = psMetadataAlloc();
+    psMetadataAddS64(addsummaryArgs, PS_LIST_TAIL, "-xcff_id", 0, "define xcff ID (required)", 0);
+    psMetadataAddStr(addsummaryArgs, PS_LIST_TAIL, "-path_base", 0, "define base output location(required)", 0);
+    psMetadataAddF32(addsummaryArgs, PS_LIST_TAIL, "-dtime_script", 0, "define elapsed time in script (seconds)", NAN);
+    psMetadataAddS16(addsummaryArgs, PS_LIST_TAIL, "-quality", 0, "set quality", 0);
+    psMetadataAddStr(addsummaryArgs, PS_LIST_TAIL, "-hostname", 0, "define hostname", 0);
+    psMetadataAddStr(addsummaryArgs, PS_LIST_TAIL, "-software_ver", 0, "define software version", 0);
+    psMetadataAddS16(addsummaryArgs, PS_LIST_TAIL, "-fault", 0, "set fault code", 0);
+
+    // -summary
+    psMetadata *summaryArgs= psMetadataAlloc();
+    psMetadataAddS64(summaryArgs, PS_LIST_TAIL, "-xcff_id", 0, "search by xcff ID", 0);
+    psMetadataAddS64(summaryArgs, PS_LIST_TAIL, "-xccal_id", 0, "search by skycal ID", 0);
+    psMetadataAddStr(summaryArgs, PS_LIST_TAIL, "-label", 0, "search by label", NULL);
+    psMetadataAddStr(summaryArgs, PS_LIST_TAIL, "-data_group", 0, "search by data_group (LIKE comparison)", NULL);
+    psMetadataAddStr(summaryArgs, PS_LIST_TAIL, "-tess_id", 0, "search by tess ID (LIKE comparison)", 0);
+    psMetadataAddStr(summaryArgs, PS_LIST_TAIL, "-skycell_id", 0, "search by skycell ID (LIKE comparison)", 0);
+    pxskycellAddArguments(summaryArgs);
+    psMetadataAddS16(summaryArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    psMetadataAddU64(summaryArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(summaryArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -revertsummary
+    psMetadata *revertsummaryArgs= psMetadataAlloc();
+    psMetadataAddS64(revertsummaryArgs, PS_LIST_TAIL, "-xcff_id", 0, "search by xcff ID", 0);
+    psMetadataAddStr(revertsummaryArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", 0);
+    psMetadataAddS16(revertsummaryArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    pxskycellAddArguments(revertsummaryArgs);
+
+    // -updatesummary
+    psMetadata *updatesummaryArgs = psMetadataAlloc();
+    psMetadataAddS64(updatesummaryArgs, PS_LIST_TAIL, "-xcff_id", 0, "define xcff ID (required)", 0);
+    psMetadataAddStr(updatesummaryArgs, PS_LIST_TAIL, "-path_base", 0, "define base output location", 0);
+    psMetadataAddF32(updatesummaryArgs, PS_LIST_TAIL, "-dtime_script", 0, "define elapsed time in script (seconds)", NAN);
+    psMetadataAddS16(updatesummaryArgs, PS_LIST_TAIL, "-quality", 0, "set quality", 0);
+    psMetadataAddStr(updatesummaryArgs, PS_LIST_TAIL, "-hostname", 0, "define hostname", 0);
+    psMetadataAddStr(updatesummaryArgs, PS_LIST_TAIL, "-software_ver", 0, "define software version", 0);
+    psMetadataAddS16(updatesummaryArgs, PS_LIST_TAIL, "-fault", 0, "set fault code", 0);
+
+    // -exportrun
+    psMetadata *exportrunArgs = psMetadataAlloc();
+    psMetadataAddS64(exportrunArgs, PS_LIST_TAIL, "-xcff_id", 0,          "export this xcff ID (required)", 0);
+    psMetadataAddStr(exportrunArgs, PS_LIST_TAIL, "-outfile", 0,          "export to this file (required)", NULL);
+    psMetadataAddU64(exportrunArgs, PS_LIST_TAIL, "-limit",   0,          "limit result set to N items", 0);
+//    psMetadataAddBool(exportrunArgs, PS_LIST_TAIL, "-clean",  0,          "export run in cleaned state", false);
+
+    // -importrun
+    psMetadata *importrunArgs = psMetadataAlloc();
+    psMetadataAddStr(importrunArgs, PS_LIST_TAIL, "-infile",  0,          "import from this file (required)", NULL);
+
+    psFree(now);
+
+    psMetadata *argSets = psMetadataAlloc();
+    psMetadata *modes   = psMetadataAlloc();
+
+    PXOPT_ADD_MODE("-definebyquery",   "Define a new xcff run",                             XCFFTOOL_MODE_DEFINEBYQUERY,   definebyqueryArgs);
+    PXOPT_ADD_MODE("-updaterun",       "Update a run",                                      XCFFTOOL_MODE_UPDATERUN,       updaterunArgs);
+    PXOPT_ADD_MODE("-todo",            "Get runs to do",                                    XCFFTOOL_MODE_TODO,            todoArgs);
+    PXOPT_ADD_MODE("-addresult",       "Add result for fullforce run on a warp",            XCFFTOOL_MODE_ADDRESULT,       addresultArgs);
+    PXOPT_ADD_MODE("-result",          "Get result fullforce run on a warp",                XCFFTOOL_MODE_RESULT,          resultArgs);
+    PXOPT_ADD_MODE("-revert",          "Revert failed fullforce run on a warp",             XCFFTOOL_MODE_REVERT,          revertArgs);
+    PXOPT_ADD_MODE("-updateresult",    "Update result for fullforce run on a warp",         XCFFTOOL_MODE_UPDATERESULT,    updateresultArgs);
+    PXOPT_ADD_MODE("-toadvance",       "list completed runs to summarize",                  XCFFTOOL_MODE_TOADVANCE,       toadvanceArgs);
+    PXOPT_ADD_MODE("-addsummary",      "insert summary result",                             XCFFTOOL_MODE_ADDSUMMARY,      addsummaryArgs);
+    PXOPT_ADD_MODE("-updatesummary",   "update summary result",                             XCFFTOOL_MODE_UPDATESUMMARY,   updatesummaryArgs);
+    PXOPT_ADD_MODE("-revertsummary",   "revert faulted summary",                            XCFFTOOL_MODE_REVERTSUMMARY,   revertsummaryArgs);
+    PXOPT_ADD_MODE("-summary",         "list summary results",                              XCFFTOOL_MODE_SUMMARY,         summaryArgs);
+    PXOPT_ADD_MODE("-exportrun",       "list summary results",                              XCFFTOOL_MODE_EXPORTRUN,       exportrunArgs);
+    PXOPT_ADD_MODE("-importrun",       "list summary results",                              XCFFTOOL_MODE_IMPORTRUN,       importrunArgs);
+
+    if (!pxGetOptions(stderr, argc, argv, config, modes, argSets)) {
+        psError(PS_ERR_UNKNOWN, true, "option parsing failed");
+        psFree(argSets);
+        psFree(modes);
+        psFree(config);
+        return NULL;
+    }
+
+    psFree(argSets);
+    psFree(modes);
+
+    // define Database handle, if used
+    // do this last so we don't setup a connection before CLI options are
+    // validated
+    config->dbh = psMemIncrRefCounter(pmConfigDB(config->modules));
+    if (!config->dbh) {
+        psError(PS_ERR_UNKNOWN, false, "Can't configure database");
+        psFree(config);
+        return NULL;
+    }
+
+    return config;
+}
Index: trunk/ippTools/src/xcskytool.c
===================================================================
--- trunk/ippTools/src/xcskytool.c	(revision 42837)
+++ trunk/ippTools/src/xcskytool.c	(revision 42837)
@@ -0,0 +1,1566 @@
+/*
+ * xcskytool.c
+ *
+ * Copyright (C) 2023 Eugene Magnier (based on staticskytool.c)
+ *
+ */
+
+#ifdef HAVB_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+#include <ippdb.h>
+
+#include "pxtools.h"
+#include "pxspace.h"
+#include "xcskytool.h"
+
+static bool definebyqueryMode(pxConfig *config);
+static bool updaterunMode(pxConfig *config);
+static bool inputsMode(pxConfig *config);
+static bool todoMode(pxConfig *config);
+static bool addresultMode(pxConfig *config);
+static bool resultMode(pxConfig *config);
+static bool revertMode(pxConfig *config);
+static bool updateresultMode(pxConfig *config);
+static bool exportrunMode(pxConfig *config);
+static bool importrunMode(pxConfig *config);
+
+static bool definexccalrunMode(pxConfig *config);
+static bool updatexccalrunMode(pxConfig *config);
+static bool pendingxccalrunMode(pxConfig *config);
+static bool addxccalresultMode(pxConfig *config);
+static bool xccalresultMode(pxConfig *config);
+static bool revertxccalresultMode(pxConfig *config);
+static bool updatexccalresultMode(pxConfig *config);
+static bool exportxccalrunMode(pxConfig *config);
+static bool importxccalrunMode(pxConfig *config);
+
+static bool setxcskyRunState(pxConfig *config, psS64 xcsky_id, const char *state);
+
+# define MODECASE(caseName, func) \
+    case caseName: \
+    if (!func(config)) { \
+        goto FAIL; \
+    } \
+    break;
+
+int main(int argc, char **argv)
+{
+    psLibInit(NULL);
+
+    //    psAbort("oops");
+
+    pxConfig *config = xcskytoolConfig(NULL, argc, argv);
+    if (!config) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to configure");
+        goto FAIL;
+    }
+
+    switch (config->mode) {
+        MODECASE(XCSKYTOOL_MODE_DEFINEBYQUERY,     definebyqueryMode);
+        MODECASE(XCSKYTOOL_MODE_UPDATERUN,         updaterunMode);
+        MODECASE(XCSKYTOOL_MODE_INPUTS,            inputsMode);
+        MODECASE(XCSKYTOOL_MODE_TODO,              todoMode);
+        MODECASE(XCSKYTOOL_MODE_ADDRESULT,         addresultMode);
+        MODECASE(XCSKYTOOL_MODE_RESULT,            resultMode);
+        MODECASE(XCSKYTOOL_MODE_REVERT,            revertMode);
+        MODECASE(XCSKYTOOL_MODE_UPDATERESULT,      updateresultMode);
+        MODECASE(XCSKYTOOL_MODE_EXPORTRUN,         exportrunMode);
+        MODECASE(XCSKYTOOL_MODE_IMPORTRUN,         importrunMode);
+        MODECASE(XCSKYTOOL_MODE_DEFINEXCCALRUN,    definexccalrunMode);
+        MODECASE(XCSKYTOOL_MODE_UPDATEXCCALRUN,    updatexccalrunMode);
+        MODECASE(XCSKYTOOL_MODE_PENDINGXCCALRUN,   pendingxccalrunMode);
+        MODECASE(XCSKYTOOL_MODE_ADDXCCALRESULT,    addxccalresultMode);
+        MODECASE(XCSKYTOOL_MODE_UPDATEXCCALRESULT, updatexccalresultMode);
+        MODECASE(XCSKYTOOL_MODE_REVERTXCCALRESULT, revertxccalresultMode);
+        MODECASE(XCSKYTOOL_MODE_XCCALRESULT,       xccalresultMode);
+        MODECASE(XCSKYTOOL_MODE_EXPORTXCCALRUN,    exportxccalrunMode);
+        MODECASE(XCSKYTOOL_MODE_IMPORTXCCALRUN,    importxccalrunMode);
+        default:
+            psAbort("invalid option (this should not happen)");
+    }
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(EXIT_SUCCESS);
+
+FAIL:
+    psErrorStackPrint(stderr, "\n");
+    int exit_status = pxerrorGetExitStatus();
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(exit_status);
+}
+
+
+static bool definebyqueryMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PXOPT_LOOKUP_STR(workdir,     config->args, "-set_workdir", true, false);
+    PXOPT_LOOKUP_STR(label,       config->args, "-set_label", true, false);
+
+    // optional
+    PXOPT_LOOKUP_STR(data_group,  config->args, "-set_data_group", false, false);
+    PXOPT_LOOKUP_STR(dist_group,  config->args, "-set_dist_group", false, false);
+    PXOPT_LOOKUP_STR(reduction,   config->args, "-set_reduction", false, false);
+    PXOPT_LOOKUP_STR(note,        config->args, "-set_note", false, false);
+    PXOPT_LOOKUP_TIME(registered, config->args, "-set_registered", false, false);
+    //    fprintf (stderr, "%s\n", registered);
+
+    psMetadata *whereMD = psMetadataAlloc();
+
+    PXOPT_COPY_S64(config->args, whereMD, "-select_xcstack_id",    "xcstackRun.xcstack_id",       "==");
+    PXOPT_COPY_STR(config->args, whereMD, "-select_tess_id",       "xcstackRun.tess_id",          "==");
+    PXOPT_COPY_STR(config->args, whereMD, "-select_skycell_id",    "xcstackRun.skycell_id",       "LIKE");
+    pxAddLabelSearchArgs(config, whereMD, "-select_filter",        "xcstackRun.filter",           "LIKE");
+    PXOPT_COPY_F32(config->args, whereMD, "-select_good_frac_min", "xcstackSumSkyfile.good_frac", ">=");
+    pxAddLabelSearchArgs(config, whereMD, "-select_label",         "xcstackRun.label",            "LIKE");
+    pxAddLabelSearchArgs(config, whereMD, "-select_data_group",    "xcstackRun.data_group",       "LIKE");
+    if (!pxskycellAddWhere(config, whereMD)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    // find the number of requested filters:
+    psMetadataItem *filters = psMetadataLookup(config->args, "-select_filter");
+    psAssert (filters, "-select_filter must exist");
+    psAssert (filters->type == PS_DATA_METADATA_MULTI, "-select_filter should be a multi container");
+    psAssert (filters->data.list->n, "-select_filter should at least have a place-holder");
+
+# if (0)
+    // XXX 'filters' is a psMetadataItem of type MULTI.  Here is code to iterate over the list of items:
+    psListIterator *myIter = psListIteratorAlloc(filters->data.list, PS_LIST_HEAD, false);
+    psMetadataItem *myItem = NULL;
+    while ((myItem = psListGetAndIncrement (myIter))) {
+      fprintf (stderr, "type: %d\n", (int) myItem->type);
+    }
+# endif
+
+    int num_filter = filters->data.list->n;
+
+    // if -select_filter was not supplied, then the 'filters' container will have a single entry
+    // with a value of NULL
+    if (num_filter == 1) {
+      // psListIterator *myIter = psListIteratorAlloc(filters->data.list, PS_LIST_HEAD, false);
+      // psMetadataItem *myItem = psListGetAndIncrement (myIter);
+
+      psMetadataItem *myItem = psListGet (filters->data.list, 0);
+      psAssert (myItem->type == PS_DATA_STRING, "unexpected data type for -select_filter");
+      if (myItem->data.str == NULL) {
+        psError(PXTOOLS_ERR_CONFIG, false, "at least one -select_filter must be supplied");
+        return false;
+      }
+    }
+
+    PXOPT_LOOKUP_BOOL(group_by_data_group, config->args, "-group_by_data_group", false);
+    PXOPT_LOOKUP_BOOL(rerun, config->args, "-rerun", false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+    PXOPT_LOOKUP_BOOL(pretend, config->args, "-pretend", false);
+    PXOPT_LOOKUP_BOOL(check_inputs, config->args, "-check_inputs", false);
+
+    psString select;
+    if (group_by_data_group) {
+        select = pxDataGet("xcskytool_definebyquery_select_by_dg.sql");
+    } else {
+        select = pxDataGet("xcskytool_definebyquery_select.sql");
+    }
+    if (!select) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        psFree(whereMD);
+        return false;
+    }
+
+    if (!psListLength(whereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        psFree(whereMD);
+        return false;
+    }
+
+    // this 'where' is used for both xcskytool_definebyquery_select.sql and xcskytool_definebyquery_inputs.sql 
+    psString where = NULL;
+    psString whereClause = psDBGenerateWhereConditionSQL(whereMD, NULL);
+    psStringAppend(&where, "\nAND %s", whereClause);
+    psFree(whereClause);
+    psFree(whereMD);
+
+    psString where2 = NULL;
+    psString make_unique = NULL;
+    if (!rerun) {
+        psStringAppend(&where2, "\n %s\nAND xcskyRun.label = '%s'", where, label);
+        psStringAppend(&make_unique, "\nAND xcsky_id IS NULL");
+    }
+
+    if (!p_psDBRunQueryF(config->dbh, select, where, where2 ? where2 : where, num_filter, make_unique ? make_unique : "")) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(select);
+        return false;
+    }
+    psFree(select);
+    psFree(where2);
+    psFree(make_unique);
+
+    // we now have a list of (tess_id, skycell_id) that (potentially) meet out needs
+    // we now need to loop over all of these and for each pair, select the best set of
+    // inputs
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        psFree(where);
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psWarning("xcskytool: no rows found");
+        psFree(output);
+        psFree(where);
+        return true;
+    }
+    if (pretend) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xcskyInput", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            psFree(where);
+            return false;
+        }
+        psFree(output);
+        psFree(where);
+        return true;
+    }
+
+    psString inputsSQL = pxDataGet("xcskytool_definebyquery_inputs.sql");
+    if (!inputsSQL) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        psFree(where);
+        return false;
+    }
+
+    for (long i = 0; i < output->n; i++) {
+        psMetadata *row = output->data[i]; // Row from select
+        bool status;
+
+        // pull out the skycell_id, tess_id, filter
+        psString skycell_id = psMetadataLookupStr(&status, row, "skycell_id");
+        if (!status) {
+            psError(PS_ERR_UNKNOWN, false, "failed to lookup skycell_id");
+            psFree(output);
+            psFree(inputsSQL);
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            return false;
+        }
+
+        psString tess_id = psMetadataLookupStr(&status, row, "tess_id");
+        if (!status) {
+            psError(PS_ERR_UNKNOWN, false, "failed to lookup tess_id");
+            psFree(output);
+            psFree(inputsSQL);
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            return false;
+        }
+        psString by_data_group = NULL;
+        psString xcstack_data_group = NULL;
+        if (group_by_data_group) {
+            xcstack_data_group = psMetadataLookupStr(&status, row, "data_group");
+            if (!status) {
+                psError(PS_ERR_UNKNOWN, false, "failed to lookup data_group");
+                psFree(output);
+                psFree(inputsSQL);
+                if (!psDBRollback(config->dbh)) {
+                    psError(PS_ERR_UNKNOWN, false, "database error");
+                }
+                return false;
+            }
+            psStringAppend(&by_data_group, "\nAND xcstackRun.data_group = '%s'", xcstack_data_group);
+        }
+
+	// query for the inputs
+	if (!p_psDBRunQueryF(config->dbh, inputsSQL, tess_id, skycell_id, where, by_data_group ? by_data_group : "")) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+            psFree(output);
+	    psFree(inputsSQL);
+	    return false;
+	}
+	
+	psArray *inputs = p_psDBFetchResult(config->dbh);
+	if (!inputs) {
+	    psErrorCode err = psErrorCodeLast();
+	    switch (err) {
+	      case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+	      case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+	      default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+	    }
+	    psFree(inputs);
+            psFree(output);
+            psFree(inputsSQL);
+	    return false;
+	}
+	if (!psArrayLength(inputs)) {
+	    psWarning("xcskytool ERROR: no rows found for known tess_id, skycell_id?");
+	    continue;
+	}
+
+	if (check_inputs) {
+	  // negative simple so the default is true
+	  if (!ippdbPrintMetadatas(stdout, inputs, "xcskyInput", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            psFree(where);
+            return false;
+	  }
+	  psFree(inputs);
+	  continue;
+	}
+
+	// XXX if we are unable to guarantee that all selected inputs have all and only the
+	// requested filters, see the code at the bottom of this file (ifdef'ed out for now)
+
+	// insert a new xcsky entry and find its new id
+	if (!psDBTransaction(config->dbh)) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    psFree(inputs);
+            psFree(output);
+            psFree(inputsSQL);
+	    return false;
+	}
+
+	// create a xcskyRun
+	if (!xcskyRunInsert(config->dbh,
+				0x0,	     // xcsky_id
+				"new",	     // state
+				workdir,
+				label,
+				data_group ? data_group : (xcstack_data_group ? xcstack_data_group : label),
+				dist_group,
+				reduction,
+			    NULL, // XXX : broken like xcstacktool : is this due to the mysql version?
+				note
+		)
+	    ) {
+	    if (!psDBRollback(config->dbh)) {
+		psError(PS_ERR_UNKNOWN, false, "database error failed to rollback transaction");
+	    }
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    psFree(inputs);
+            psFree(output);
+            psFree(inputsSQL);
+	    return false;
+	}
+
+	psS64 xcsky_id =  psDBLastInsertID(config->dbh);
+
+	// loop over the possible inputs and record only the valid ones
+	for (int j = 0; j < inputs->n; j++) {
+	    psMetadata *inputRow = inputs->data[j]; // Row from select
+	    
+	    // pull out the skycell_id, tess_id, filter
+	    psS64 xcstack_id = psMetadataLookupS64(&status, inputRow, "xcstack_id");
+	    psAssert(status, "failed to find xcstack_id?");
+	    
+	    // add a xcskyInput entry
+	    if (!xcskyInputInsert(config->dbh, xcsky_id, xcstack_id)) {
+		if (!psDBRollback(config->dbh)) {
+		    psError(PS_ERR_UNKNOWN, false, "database error failed to rollback transaction");
+		}
+		psError(PS_ERR_UNKNOWN, false, "database error");
+		psFree(inputs);
+		psFree(output);
+		psFree(inputsSQL);
+		return false;
+	    }
+	}
+	psFree (inputs);
+	
+	if (!psDBCommit(config->dbh)) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    psFree(output);
+	    psFree(inputsSQL);
+	    return false;
+	}
+    }
+    psFree(inputsSQL);
+    psFree(output);
+    return true;
+}
+
+static bool updaterunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcsky_id",     "xcsky_id",            "==");
+    PXOPT_COPY_STR(config->args, where, "-label",       "xcskyRun.label",      "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group",  "xcskyRun.data_group", "==");
+    PXOPT_COPY_STR(config->args, where, "-state",       "xcskyRun.state",      "==");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",     "xcstackRun.tess_id",    "==");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id",  "xcstackRun.skycell_id", "==");
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    psString query = psStringCopy("UPDATE xcskyRun JOIN xcskyInput USING(xcsky_id) JOIN xcstackRun using(xcstack_id) JOIN skycell USING(tess_id, skycell_id)");
+
+    // pxUpdateRun gets parameters from config->args and updates
+    bool result = pxUpdateRun(config, where, &query, "xcskyRun", "xcsky_id", "xcskyResult", true, false);
+    psFree(query);
+    psFree(where);
+
+    return result;
+}
+
+static bool inputsMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // require at least an xcsky id (add better search options)
+    PXOPT_LOOKUP_S64(xcsky_id, config->args, "-xcsky_id", true, false);
+
+    psMetadata *where = psMetadataAlloc();
+    psMetadataAddS64 (where, PS_LIST_TAIL, "xcsky_id", 0, "==", xcsky_id);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcskytool_inputs.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, "xcskyInput");
+        psStringAppend(&query, " WHERE %s", whereClause);
+        psFree(whereClause);
+    }
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcskytool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xcskyInput", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+
+    return true;
+}
+
+static bool todoMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *whereMD = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, whereMD, "-xcsky_id", "xcsky_id", "==");
+    pxAddLabelSearchArgs (config, whereMD, "-label", "xcskyRun.label", "==");
+    pxskycellAddWhere(config, whereMD);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcskytool_todo.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (!psListLength(whereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        psFree(whereMD);
+        return false;
+    }
+
+    psString where = NULL;
+    psString whereClause = psDBGenerateWhereConditionSQL(whereMD, NULL);
+    psStringAppend(&where, "\n AND %s", whereClause);
+    psFree(whereClause);
+    psFree(whereMD);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    // the where clause is required and matches the WHERE hook format string
+    if (!p_psDBRunQueryF(config->dbh, query, where)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcskytool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xcskyResult", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool addresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required
+    PXOPT_LOOKUP_S64(xcsky_id, config->args, "-xcsky_id", true, false);
+
+    // optional
+    PXOPT_LOOKUP_STR(path_base, config->args, "-path_base", false, false);
+    PXOPT_LOOKUP_F32(dtime_phot, config->args, "-dtime_phot", false, false);
+    PXOPT_LOOKUP_F32(dtime_script, config->args, "-dtime_script", false, false);
+    PXOPT_LOOKUP_S32(sources, config->args, "-sources", false, false);
+    PXOPT_LOOKUP_STR(hostname, config->args, "-hostname", false, false);
+    PXOPT_LOOKUP_F32(good_frac, config->args, "-good_frac", false, false);
+    PXOPT_LOOKUP_S32(num_inputs, config->args, "-num_inputs", false, false);
+
+    // default values
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", false, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-quality", false, false);
+
+    // XXX not sure we need a transaction here...
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (!xcskyResultInsert(config->dbh,
+			       xcsky_id,
+                               path_base,
+                               dtime_phot,
+                               dtime_script,
+                               sources,
+			       num_inputs,
+                               hostname,
+                               good_frac,
+                               fault,
+                               quality
+          )) {
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+        }
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (!fault) {
+        if (!setxcskyRunState(config, xcsky_id, "full")) {
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            psError(PS_ERR_UNKNOWN, false, "failed to change xcskyRun state");
+            return false;
+        }
+    }
+
+    // point of no return
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+
+// XXX what is this used by?  what filters are needed?
+static bool resultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcsky_id",     "xcskyRun.xcsky_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id",   "xcskyInput.xcstack_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-label",        "xcskyRun.label", "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group",   "xcskyRun.data_group", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",      "xcstackRun.tess_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id",   "xcstackRun.skycell_id", "LIKE");
+    PXOPT_COPY_S16(config->args, where, "-fault",        "xcskyResult.fault", "==");
+    pxskycellAddWhere(config, where);
+    PXOPT_LOOKUP_S32(num_filters, config->args, "-num_filters", false, false);
+
+    PXOPT_LOOKUP_BOOL(all, config->args, "-all", false);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcskytool_result.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " WHERE %s", whereClause);
+        psFree(whereClause);
+    } else if (!all) {
+        psError(PXTOOLS_ERR_CONFIG, true, "search parameters or -all are required");
+        return false;
+    }
+
+    psFree(where);
+
+    psStringAppend(&query, "\nGROUP BY xcsky_id");
+    if (num_filters) {
+        psStringAppend(&query, "\nHAVING COUNT(filter) = %d", num_filters);
+    }
+        
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, "\n%s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcskytool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        if (!ippdbPrintMetadatas(stdout, output, "xcskyResult", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+
+    return true;
+}
+
+static bool revertMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcsky_id", "xcskyResult.xcsky_id", "==");
+    pxAddLabelSearchArgs(config, where, "-label", "xcskyRun.label", "==");
+    PXOPT_COPY_S16(config->args, where, "-fault", "xcskyResult.fault", "==");
+
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    if (!psListLength(where->list) && !psMetadataLookupBool(NULL, config->args, "-all")) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    psString delete = pxDataGet("xcskytool_revert.sql");
+    if (!delete) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+    psString update = pxDataGet("xcskytool_revert_update.sql");
+    if (!update) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&delete, " AND %s", whereClause);
+        psStringAppend(&update, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, delete)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(delete);
+        psFree(where);
+        return false;
+    }
+    psFree(delete);
+
+    int numRows = psDBAffectedRows(config->dbh);
+    psLogMsg("xcskytool", PS_LOG_INFO, "Deleted %d rows", numRows);
+
+    if (!p_psDBRunQuery(config->dbh, update)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(delete);
+        psFree(where);
+        return false;
+    }
+    psFree(update);
+
+    numRows = psDBAffectedRows(config->dbh);
+    psLogMsg("xcskytool", PS_LOG_INFO, "Updated %d rows", numRows);
+
+    psFree(where);
+    return true;
+}
+
+static bool updateresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", true, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-set_quality", false, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcsky_id",   "xcsky_id",   "==");
+
+    if (!pxSetFaultCode(config->dbh, "xcskyResult", where, fault, quality)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to set set fault flag");
+        psFree (where);
+        return false;
+    }
+    psFree (where);
+    return true;
+}
+
+bool exportrunMode(pxConfig *config)
+{
+  typedef struct ExportTable {
+    char tableName[80];
+    char sqlFilename[80];
+  } ExportTable;
+
+  int numExportTables = 3;
+
+  PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+  // XXX unused PXOPT_LOOKUP_S64(det_id, config->args, "-xcsky_id", true,  false);
+  PXOPT_LOOKUP_STR(outfile, config->args, "-outfile", true,  false);
+  PXOPT_LOOKUP_BOOL(clean,  config->args, "-clean",   false);
+  PXOPT_LOOKUP_U64(limit,   config->args, "-limit",   false, false);
+
+  FILE *f = fopen (outfile, "w");
+  if (f == NULL) {
+      psError(PS_ERR_UNKNOWN, false, "failed to open output file");
+      return false;
+  }
+
+  if (!pxExportVersion(config, f)) {
+    psError(PS_ERR_UNKNOWN, false, "failed to write dbversion output file");
+    return false;
+  }
+  psMetadata *where = psMetadataAlloc();
+  PXOPT_COPY_S64(config->args, where, "-xcsky_id", "xcsky_id", "==");
+
+  ExportTable tables [] = {
+    {"xcskyRun", "xcskytool_export_run.sql"},
+    {"xcskyInput", "xcskytool_export_input.sql"},
+    {"xcskyResult", "xcskytool_export_result.sql"},
+  };
+
+  for (int i=0; i < numExportTables; i++) {
+    psString query = pxDataGet(tables[i].sqlFilename);
+    if (!query) {
+      psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+      return false;
+    }
+
+    if (where && psListLength(where->list)) {
+      psString whereClause = psDBGenerateWhereSQL(where, NULL);
+      psStringAppend(&query, " %s", whereClause);
+      psFree(whereClause);
+    }
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+      psString limitString = psDBGenerateLimitSQL(limit);
+      psStringAppend(&query, " %s", limitString);
+      psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      psFree(query);
+      return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      return false;
+    }
+    if (!psArrayLength(output)) {
+      psError(PS_ERR_UNKNOWN, true, "no rows found");
+      psFree(output);
+      return false;
+    }
+
+    if (clean) {
+        if (!strcmp(tables[i].tableName, "xcskyRun")) {
+            if (!pxSetStateCleaned("xcskyRun", "state", output)) {
+                psFree(output);
+                psError(PS_ERR_UNKNOWN, false, "pxSetStateClean failed for table %s",  tables[i].tableName);
+                return false;
+            }
+        }
+    }
+
+      // we must write the export table in non-simple (true) format
+    if (!ippdbPrintMetadatas(f, output, tables[i].tableName, true)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to print array");
+        psFree(output);
+        return false;
+    }
+
+    psFree(output);
+  }
+
+  fclose (f);
+  return true;
+}
+
+bool importrunMode(pxConfig *config)
+{
+  return false;
+}
+
+bool exportxccalrunMode(pxConfig *config)
+{
+  typedef struct ExportTable {
+    char tableName[80];
+    char sqlFilename[80];
+  } ExportTable;
+
+  int numExportTables = 2;
+
+  PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+  // XXX unused PXOPT_LOOKUP_S64(det_id, config->args, "-xcsky_id", true,  false);
+  PXOPT_LOOKUP_STR(outfile, config->args, "-outfile", true,  false);
+  PXOPT_LOOKUP_BOOL(clean,  config->args, "-clean",   false);
+  PXOPT_LOOKUP_U64(limit,   config->args, "-limit",   false, false);
+
+  FILE *f = fopen (outfile, "w");
+  if (f == NULL) {
+      psError(PS_ERR_UNKNOWN, false, "failed to open output file");
+      return false;
+  }
+
+  if (!pxExportVersion(config, f)) {
+    psError(PS_ERR_UNKNOWN, false, "failed to write dbversion output file");
+    return false;
+  }
+  psMetadata *where = psMetadataAlloc();
+  PXOPT_COPY_S64(config->args, where, "-xccal_id", "xccal_id", "==");
+
+  ExportTable tables [] = {
+    {"xccalRun", "xcskytool_export_xccalrun.sql"},
+    {"xccalResult", "xcskytool_export_xccalresult.sql"},
+  };
+
+  for (int i = 0; i < numExportTables; i++) {
+    psString query = pxDataGet(tables[i].sqlFilename);
+    if (!query) {
+      psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+      return false;
+    }
+
+    if (where && psListLength(where->list)) {
+      psString whereClause = psDBGenerateWhereSQL(where, NULL);
+      psStringAppend(&query, " %s", whereClause);
+      psFree(whereClause);
+    }
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+      psString limitString = psDBGenerateLimitSQL(limit);
+      psStringAppend(&query, " %s", limitString);
+      psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      psFree(query);
+      return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      return false;
+    }
+    if (!psArrayLength(output)) {
+      psError(PS_ERR_UNKNOWN, true, "no rows found");
+      psFree(output);
+      return false;
+    }
+
+    if (clean) {
+        if (!strcmp(tables[i].tableName, "xccalRun")) {
+            if (!pxSetStateCleaned("xccalRun", "state", output)) {
+                psFree(output);
+                psError(PS_ERR_UNKNOWN, false, "pxSetStateClean failed for table %s",  tables[i].tableName);
+                return false;
+            }
+        }
+    }
+
+      // we must write the export table in non-simple (true) format
+    if (!ippdbPrintMetadatas(f, output, tables[i].tableName, true)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to print array");
+        psFree(output);
+        return false;
+    }
+
+    psFree(output);
+  }
+
+  fclose (f);
+  return true;
+}
+
+bool importxccalrunMode(pxConfig *config)
+{
+  return false;
+}
+
+static bool setxcskyRunState(pxConfig *config, psS64 xcsky_id, const char *state)
+{
+    PS_ASSERT_PTR_NON_NULL(state, false);
+
+    // check that state is a valid string value
+    if (!pxIsValidState(state)) {
+        psError(PS_ERR_UNKNOWN, false, "invalid xcskyRun state: %s", state);
+        return false;
+    }
+
+    char *query = "UPDATE xcskyRun SET state = '%s' WHERE xcsky_id = %"PRId64;
+    if (!p_psDBRunQueryF(config->dbh, query, state, xcsky_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to change state for xcsky_id %"PRId64, xcsky_id);
+        return false;
+    }
+    return true;
+
+}
+
+static bool definexccalrunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required options
+    // none required. We get workdir, etc from xcskyRun if not provided
+
+    // optional
+    PXOPT_LOOKUP_STR(label,       config->args, "-set_label", false, false);
+    PXOPT_LOOKUP_STR(workdir,     config->args, "-set_workdir", false, false);
+    PXOPT_LOOKUP_STR(data_group,  config->args, "-set_data_group", false, false);
+    PXOPT_LOOKUP_STR(dist_group,  config->args, "-set_dist_group", false, false);
+    PXOPT_LOOKUP_STR(reduction,   config->args, "-set_reduction", false, false);
+    PXOPT_LOOKUP_STR(note,        config->args, "-set_note", false, false);
+    PXOPT_LOOKUP_TIME(registered, config->args, "-set_registered", false, false);
+
+    psMetadata *whereMD = psMetadataAlloc();
+
+    PXOPT_COPY_S64(config->args, whereMD, "-select_xcsky_id",      "xcskyRun.xcsky_id",           "==");
+    PXOPT_COPY_S64(config->args, whereMD, "-select_xcstack_id",    "xcstackRun.xcstack_id",       "==");
+    PXOPT_COPY_STR(config->args, whereMD, "-select_tess_id",       "xcstackRun.tess_id",          "==");
+    PXOPT_COPY_STR(config->args, whereMD, "-select_skycell_id",    "xcstackRun.skycell_id",       "LIKE");
+    pxAddLabelSearchArgs(config, whereMD, "-select_filter",        "xcstackRun.filter",           "LIKE");
+    PXOPT_COPY_F32(config->args, whereMD, "-select_good_frac_min", "xcstackSumSkyfile.good_frac", ">=");
+    pxAddLabelSearchArgs(config, whereMD, "-select_label",         "xcskyRun.label",              "LIKE");
+    pxAddLabelSearchArgs(config, whereMD, "-select_data_group",    "xcskyRun.data_group",         "LIKE");
+    if (!pxskycellAddWhere(config, whereMD)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    PXOPT_LOOKUP_BOOL(rerun, config->args, "-rerun", false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+    PXOPT_LOOKUP_BOOL(pretend, config->args, "-pretend", false);
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+
+    psString query = pxDataGet("xcskytool_definexccalrun.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        psFree(whereMD);
+        return false;
+    }
+
+    if (!psListLength(whereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        psFree(whereMD);
+        return false;
+    }
+
+    psString whereClause = psDBGenerateWhereConditionSQL(whereMD, NULL);
+    psStringAppend(&query, "\nAND %s", whereClause);
+    psFree(whereClause);
+    psFree(whereMD);
+
+    psString labelHook = psStringCopy("");
+    if (!rerun) {
+        if (label)  {
+            psStringAppend(&labelHook, "\nAND xccalRun.label = '%s'", label);
+        }
+        psStringAppend(&query, "\nAND xccal_id IS NULL");
+    }
+
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, "\n%s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQueryF(config->dbh, query, labelHook)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(labelHook);
+        psFree(query);
+        return false;
+    }
+    psFree(labelHook);
+    psFree(query);
+
+    // we now have a list of (tess_id, skycell_id) that (potentially) meet out needs
+    // we now need to loop over all of these and for each pair, select the best set of
+    // inputs
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psWarning("xcskytool: no rows found");
+        psFree(output);
+        return true;
+    }
+    if (pretend) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xccalRun", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+        psFree(output);
+        return true;
+    }
+
+    for (long i = 0; i < output->n; i++) {
+        psMetadata *row = output->data[i]; // Row from select
+        bool status;
+
+        psS64 xcsky_id = psMetadataLookupS64(&status, row, "xcsky_id");
+        psS64 xcstack_id = psMetadataLookupS64(&status, row, "xcstack_id");
+        psString sky_workdir = psMetadataLookupStr(&status, row, "workdir");
+        psString sky_label =  psMetadataLookupStr(&status, row, "label");
+        psString sky_data_group =  psMetadataLookupStr(&status, row, "data_group");
+        psString sky_dist_group =  psMetadataLookupStr(&status, row, "dist_group");
+
+	// create a xcskyRun
+	if (!xccalRunInsert(config->dbh,
+				0x0,	     // xccal_id
+                                xcsky_id,
+                                xcstack_id,
+				"new",	     // state
+				workdir ? workdir : sky_workdir,
+				label ? label : sky_label,
+				data_group ? data_group : sky_data_group,
+				dist_group ? dist_group : sky_dist_group,
+				reduction,
+				NULL,
+				note
+		)
+	    ) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+            psFree(output);
+	    return false;
+	}
+
+    }
+    psFree(output);
+    return true;
+}
+
+static bool updatexccalrunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xccal_id", "xccal_id",   "==");
+    PXOPT_COPY_S64(config->args, where, "-xcsky_id",   "xcsky_id",   "==");
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id", "xcstack_id",   "==");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id",  "xcstackRun.skycell_id",    "==");
+    PXOPT_COPY_STR(config->args, where, "-label",   "xccalRun.label",    "==");
+    PXOPT_COPY_STR(config->args, where, "-state",   "xccalRun.state",    "==");
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    psString query = psStringCopy("UPDATE xccalRun JOIN xcstackRun using(xcstack_id) join skycell using(tess_id, skycell_id)");
+
+    // pxUpdateRun gets parameters from config->args and updates
+    bool result = pxUpdateRun(config, where, &query, "xccalRun", "xccal_id", "xccalResult", true, false);
+    psFree(query);
+    psFree(where);
+
+    return result;
+}
+
+static bool pendingxccalrunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *whereMD = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, whereMD, "-xccal_id", "xccal_id", "==");
+    PXOPT_COPY_S64(config->args, whereMD, "-xcsky_id", "xcsky_id", "==");
+    pxAddLabelSearchArgs (config, whereMD, "-label", "xccalRun.label", "==");
+    PXOPT_COPY_STR(config->args, whereMD, "-filter", "xcstackRun.filter", "LIKE");
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcskytool_pendingxccalrun.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (!psListLength(whereMD->list)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        psFree(whereMD);
+        return false;
+    }
+
+    psString whereClause = psDBGenerateWhereConditionSQL(whereMD, NULL);
+    psStringAppend(&query, "\n AND %s", whereClause);
+    psFree(whereClause);
+    psFree(whereMD);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, "\n%s", limitString);
+        psFree(limitString);
+    }
+
+    // the where clause is required and matches the WHERE hook format string
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcskytool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "pendingxccalRun", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool addxccalresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required
+    PXOPT_LOOKUP_S64(xccal_id, config->args, "-xccal_id", true, false);
+
+    // optional
+    PXOPT_LOOKUP_F32(sigma_ra, config->args,  "-sigma_ra", false, false);
+    PXOPT_LOOKUP_F32(sigma_dec, config->args, "-sigma_dec", false, false);
+
+    PXOPT_LOOKUP_F32(zpt_obs, config->args,   "-zpt_obs", false, false);
+    PXOPT_LOOKUP_F32(zpt_stdev, config->args, "-zpt_stdev", false, false);
+    PXOPT_LOOKUP_F32(fwhm_major, config->args, "-fwhm_major", false, false);
+    PXOPT_LOOKUP_F32(fwhm_minor, config->args, "-fwhm_minor", false, false);
+
+
+    PXOPT_LOOKUP_F32(dtime_script, config->args,   "-dtime_script", false, false);
+    PXOPT_LOOKUP_F32(dtime_astrom, config->args,   "-dtime_astrom", false, false);
+
+    PXOPT_LOOKUP_STR(hostname, config->args,    "-hostname", false, false);
+    PXOPT_LOOKUP_S32(n_astrom, config->args,    "-n_astrom", false, false);
+    PXOPT_LOOKUP_S32(n_detections, config->args,"-n_detections", false, false);
+    PXOPT_LOOKUP_S32(n_extended, config->args,  "-n_extended", false, false);
+    PXOPT_LOOKUP_S32(n_forced, config->args,    "-n_forced", false, false);
+
+    PXOPT_LOOKUP_STR(path_base, config->args,   "-path_base", false, false);
+    PXOPT_LOOKUP_S16(fault, config->args,       "-fault", false, false);
+    PXOPT_LOOKUP_S16(quality, config->args,     "-quality", false, false);
+
+    PXOPT_LOOKUP_STR(ver_pslib, config->args,   "-ver_pslib", false, false);
+    PXOPT_LOOKUP_STR(ver_psmodules, config->args, "-ver_psmodules", false, false);
+    PXOPT_LOOKUP_STR(ver_psphot, config->args,  "-ver_psphot", false, false);
+    PXOPT_LOOKUP_STR(ver_psastro, config->args, "-ver_psastro", false, false);
+    PXOPT_LOOKUP_STR(ver_ppstats, config->args, "-ver_ppstats", false, false);
+
+    psString software_ver = NULL;
+    if ((ver_pslib)&&(ver_psmodules)) {
+      software_ver = pxMergeCodeVersions(ver_pslib,ver_psmodules);
+    }
+    if (ver_psphot) {
+      software_ver = pxMergeCodeVersions(software_ver,ver_psphot);
+    }
+    if (ver_psastro) {
+      software_ver = pxMergeCodeVersions(software_ver,ver_psastro);
+    }
+    if (ver_ppstats) {
+      software_ver = pxMergeCodeVersions(software_ver,ver_ppstats);
+    }
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    xccalResultRow *row = xccalResultRowAlloc(
+        xccal_id,
+        path_base,
+        dtime_script,
+        dtime_astrom,
+        sigma_ra,
+        sigma_dec,
+        n_astrom,
+        n_detections,
+        n_extended,
+        n_forced,
+        zpt_obs,
+        zpt_stdev,
+        fwhm_major,
+        fwhm_minor,
+        quality,
+        software_ver,
+        hostname,
+        fault
+        );
+
+    if (!xccalResultInsertObject(config->dbh, row)) {
+        // rollback
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+        }
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    if (fault) {
+        if (!psDBCommit(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+            return false;
+        }
+        return true;
+    }
+
+    psString query = "UPDATE xccalRun SET state = 'full' WHERE xccal_id = %" PRId64;
+    if (!p_psDBRunQueryF(config->dbh, query, xccal_id)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+static bool updatexccalresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    PXOPT_LOOKUP_S16(fault, config->args, "-set_fault", true, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-set_quality", false, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xccal_id",   "xccal_id",   "==");
+
+    if (!pxSetFaultCode(config->dbh, "xccalResult", where, fault, quality)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to set set fault flag");
+        psFree (where);
+        return false;
+    }
+    psFree (where);
+    return true;
+}
+static bool xccalresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xccal_id",  "xccalRun.xccal_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-xcsky_id",     "xccalRun.xcsky_id", "==");
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id",   "xcstackRun.xcstack_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",    "xcstackRun.tess_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id", "xcstackRun.skycell_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-filter",     "xcstackRun.filter", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-label",      "xccalRun.label", "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group", "xccalRun.data_group", "LIKE");
+    PXOPT_COPY_S16(config->args, where, "-quality",     "xccalResult.quality", "==");
+    PXOPT_COPY_S16(config->args, where, "-fault",      "xccalResult.fault", "==");
+
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcskytool_xccalresult.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (!psListLength(where->list)) {
+        psError(PXTOOLS_ERR_CONFIG, true, "search parameters are required");
+        return false;
+    }
+    psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+    psStringAppend(&query, " WHERE %s", whereClause);
+    psFree(whereClause);
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcskytool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (psArrayLength(output)) {
+        if (!ippdbPrintMetadatas(stdout, output, "xcskyResult", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+    }
+
+    psFree(output);
+
+    return true;
+}
+static bool revertxccalresultMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xccal_id", "xccalResult.xccal_id", "==");
+    pxAddLabelSearchArgs(config, where, "-label", "xccalRun.label", "==");
+    pxAddLabelSearchArgs(config, where, "-data_group", "xccalRun.data_group", "==");
+    pxAddLabelSearchArgs(config, where, "-filter", "xcstackRun.filter", "==");
+    PXOPT_COPY_S16(config->args, where, "-fault", "xccalResult.fault", "==");
+    if (!pxskycellAddWhere(config, where)) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to add skycell search arguments");
+        return false;
+    }
+
+    if (!psListLength(where->list) && !psMetadataLookupBool(NULL, config->args, "-all")) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    // Delete product
+    psString delete = pxDataGet("xcskytool_revertxccal.sql");
+    if (!delete) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&delete, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, delete)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(delete);
+        psFree(where);
+        return false;
+    }
+    psFree(delete);
+
+    int numRows = psDBAffectedRows(config->dbh); // Number of row affected
+    psLogMsg("xcskytool", PS_LOG_INFO, "Deleted %d rows", numRows);
+
+    psFree(where);
+    return true;
+}
Index: trunk/ippTools/src/xcskytool.h
===================================================================
--- trunk/ippTools/src/xcskytool.h	(revision 42837)
+++ trunk/ippTools/src/xcskytool.h	(revision 42837)
@@ -0,0 +1,39 @@
+/*
+ * xcskytool.h
+ *
+ * Copyright (C) 2023  Eugene Magnier (based on staticskytool.h)
+ *
+ */
+
+#ifndef XCSKYTOOL_H
+#define XCSKYTOOL_H 1
+
+#include "pxtools.h"
+
+typedef enum {
+    XCSKYTOOL_MODE_NONE           = 0x0,
+    XCSKYTOOL_MODE_DEFINEBYQUERY,
+    XCSKYTOOL_MODE_UPDATERUN,
+    XCSKYTOOL_MODE_INPUTS,
+    XCSKYTOOL_MODE_TODO,
+    XCSKYTOOL_MODE_ADDRESULT,
+    XCSKYTOOL_MODE_RESULT,
+    XCSKYTOOL_MODE_REVERT,
+    XCSKYTOOL_MODE_UPDATERESULT,
+    XCSKYTOOL_MODE_EXPORTRUN,
+    XCSKYTOOL_MODE_IMPORTRUN,
+
+    XCSKYTOOL_MODE_DEFINEXCCALRUN,
+    XCSKYTOOL_MODE_PENDINGXCCALRUN,
+    XCSKYTOOL_MODE_UPDATEXCCALRUN,
+    XCSKYTOOL_MODE_ADDXCCALRESULT,
+    XCSKYTOOL_MODE_UPDATEXCCALRESULT,
+    XCSKYTOOL_MODE_XCCALRESULT,
+    XCSKYTOOL_MODE_REVERTXCCALRESULT,
+    XCSKYTOOL_MODE_EXPORTXCCALRUN,
+    XCSKYTOOL_MODE_IMPORTXCCALRUN,
+} xcskytoolMode;
+
+pxConfig *xcskytoolConfig(pxConfig *config, int argc, char **argv);
+
+#endif // XCSKYTOOL_H
Index: trunk/ippTools/src/xcskytoolConfig.c
===================================================================
--- trunk/ippTools/src/xcskytoolConfig.c	(revision 42837)
+++ trunk/ippTools/src/xcskytoolConfig.c	(revision 42837)
@@ -0,0 +1,305 @@
+/*
+ * xcskytoolConfig.c
+ *
+ * Copyright (C) 2023 Eugene Magnier (based on staticskytoolConfig.c)
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <psmodules.h>
+
+#include "pxtools.h"
+#include "xcskytool.h"
+
+pxConfig *xcskytoolConfig(pxConfig *config, int argc, char **argv)
+{
+    if (!config) { config = pxConfigAlloc(); }
+
+    pmConfigReadParamsSet(false);
+
+    // setup site config
+    config->modules = pmConfigRead(&argc, argv, NULL);
+    if (!config->modules) {
+        psError(psErrorCodeLast(), false, "Can't find site configuration");
+        psFree(config);
+        return NULL;
+    }
+
+    psTime *now = psTimeGetNow(PS_TIME_TAI);
+
+    // -definebyquery
+    psMetadata *definebyqueryArgs = psMetadataAlloc();
+    psMetadataAddS64(definebyqueryArgs,  PS_LIST_TAIL, "-select_xcstack_id", 0, "search for xcstack_id", 0);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_skycell_id", 0, "search for skycell_id", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_tess_id", 0, "search for tess_id", NULL);
+    psMetadataAddF32(definebyqueryArgs,  PS_LIST_TAIL, "-select_good_frac_min", 0, "define min good_frac", NAN);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_label", PS_META_DUPLICATE_OK, "search by xcstackRun label (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_data_group", PS_META_DUPLICATE_OK, "search by xcstackRun data_group (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_filter", PS_META_DUPLICATE_OK, "search by filter (LIKE comparison, multiple OK)", NULL);
+    pxskycellAddArguments(definebyqueryArgs);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_workdir", 0, "define workdir (required)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_label", 0, "define label (required)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_data_group", 0, "define data group", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_dist_group", 0, "define dist group", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_note", 0, "define note", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_reduction", 0, "define reduction", NULL);
+    psMetadataAddTime(definebyqueryArgs, PS_LIST_TAIL, "-set_registered", 0, "time detrend run was registered", now);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-group_by_data_group",  0, "queue unique run for each data_group", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-rerun",  0, "queue new run even if one already exists for inputs", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-pretend",  0, "do not actually modify the database", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-check_inputs",  0, "list inputs, do not modify database", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -updaterun
+    psMetadata *updaterunArgs = psMetadataAlloc();
+    psMetadataAddS64(updaterunArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-state", 0, "search by state", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-label", 0, "search by label", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-data_group", 0, "search by data_group", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-tess_id", 0, "search by tess_id", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-skycell_id", 0, "search by skycell_id", NULL);
+    pxskycellAddArguments(updaterunArgs);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_label", 0, "define new value for label", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_state", 0, "define new state", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_data_group", 0, "define new data_group", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_dist_group", 0, "define new dist_group", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_note", 0, "define new note", NULL);
+
+    // -inputs
+    psMetadata *inputsArgs = psMetadataAlloc();
+    psMetadataAddS64(inputsArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddU64(inputsArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(inputsArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -todo
+    psMetadata *todoArgs = psMetadataAlloc();
+    psMetadataAddS64(todoArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddStr(todoArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", NULL);
+    pxskycellAddArguments(todoArgs);
+    psMetadataAddU64(todoArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(todoArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -addresult
+    psMetadata *addresultArgs = psMetadataAlloc();
+    psMetadataAddS64(addresultArgs, PS_LIST_TAIL, "-xcsky_id", 0, "define xcsky ID (required)", 0);
+    psMetadataAddStr(addresultArgs, PS_LIST_TAIL, "-path_base", 0, "define base output location", 0);
+    psMetadataAddF32(addresultArgs, PS_LIST_TAIL, "-dtime_phot", 0, "define photometry time", NAN);
+    psMetadataAddF32(addresultArgs, PS_LIST_TAIL, "-dtime_script", 0, "define elapsed time in script (seconds)", NAN);
+    psMetadataAddS32(addresultArgs, PS_LIST_TAIL, "-sources", 0, "number of sources", 0);
+    psMetadataAddS32(addresultArgs, PS_LIST_TAIL, "-num_inputs", 0, "number of inputs", 0);
+    psMetadataAddStr(addresultArgs, PS_LIST_TAIL, "-hostname", 0, "define hostname", 0);
+    psMetadataAddF32(addresultArgs, PS_LIST_TAIL, "-good_frac", 0, "define fraction of good pixels", NAN);
+    psMetadataAddS16(addresultArgs, PS_LIST_TAIL, "-quality", 0, "set quality", 0);
+    psMetadataAddS16(addresultArgs, PS_LIST_TAIL, "-fault", 0, "set fault code", 0);
+
+    // -result
+    psMetadata *resultArgs= psMetadataAlloc();
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-tess_id", 0, "search by tess ID (LIKE comparison)", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-skycell_id", 0, "search by skycell ID (LIKE comparison)", 0);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-label", 0, "search by label", NULL);
+    psMetadataAddStr(resultArgs, PS_LIST_TAIL, "-data_group", 0, "search by data_group (LIKE comparison)", NULL);
+    psMetadataAddS16(resultArgs, PS_LIST_TAIL, "-num_filters", 0, "search by number of filters in the inputs", 0);
+    psMetadataAddS64(resultArgs, PS_LIST_TAIL, "-xcstack_id", 0, "search by input xcstack ID (if used num_filters will be wrong)", 0);
+    pxskycellAddArguments(resultArgs);
+    psMetadataAddS16(resultArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    psMetadataAddU64(resultArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(resultArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+    psMetadataAddBool(resultArgs, PS_LIST_TAIL, "-all", 0, "allow no search terms", false);
+
+    // -revert
+    psMetadata *revertArgs= psMetadataAlloc();
+    psMetadataAddS64(revertArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddStr(revertArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", 0);
+    psMetadataAddS16(revertArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    pxskycellAddArguments(revertArgs);
+    psMetadataAddBool(revertArgs, PS_LIST_TAIL, "-all", 0, "allow no search terms", false);
+
+    // -updateresult
+    psMetadata *updateresultArgs = psMetadataAlloc();
+    psMetadataAddS64(updateresultArgs, PS_LIST_TAIL, "-xcsky_id", 0, "define xcsky ID (required)", 0);
+    psMetadataAddS16(updateresultArgs, PS_LIST_TAIL, "-fault", 0, "set fault code (required)", 0);
+    psMetadataAddS16(updateresultArgs, PS_LIST_TAIL, "-set_quality",  0, "set quality", 0);
+
+    // -exportrun
+    psMetadata *exportrunArgs = psMetadataAlloc();
+    psMetadataAddS64(exportrunArgs, PS_LIST_TAIL, "-xcsky_id", 0, "export this xcsky ID (required)", 0);
+    psMetadataAddStr(exportrunArgs, PS_LIST_TAIL, "-outfile", 0, "export to this file (required)", NULL);
+    psMetadataAddU64(exportrunArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(exportrunArgs, PS_LIST_TAIL, "-clean", 0, "mark tables as cleaned", false);
+
+    // -importrun
+    psMetadata *importrunArgs = psMetadataAlloc();
+    psMetadataAddStr(importrunArgs, PS_LIST_TAIL, "-infile", 0, "import from this file (required)", NULL);
+  
+    // -definexccalrun
+    psMetadata *definexccalrunArgs = psMetadataAlloc();
+    psMetadataAddS64(definexccalrunArgs,  PS_LIST_TAIL, "-select_xcsky_id", 0, "search for xcsky_id", 0);
+    psMetadataAddS64(definexccalrunArgs,  PS_LIST_TAIL, "-select_xcstack_id", 0, "search for xcstack_id", 0);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-select_skycell_id", 0, "search for skycell_id", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-select_tess_id", 0, "search for tess_id", NULL);
+    psMetadataAddF32(definexccalrunArgs,  PS_LIST_TAIL, "-select_good_frac_min", 0, "define min good_frac", NAN);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-select_label", PS_META_DUPLICATE_OK, "search by xcstackRun label (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-select_data_group", PS_META_DUPLICATE_OK, "search by xcstackRun data_group (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-select_filter", PS_META_DUPLICATE_OK, "search by filter (LIKE comparison, multiple OK)", NULL);
+    pxskycellAddArguments(definexccalrunArgs);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-set_workdir", 0, "define workdir", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-set_label", 0, "define label", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-set_data_group", 0, "define data group", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-set_dist_group", 0, "define dist group", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-set_note", 0, "define note", NULL);
+    psMetadataAddStr(definexccalrunArgs,  PS_LIST_TAIL, "-set_reduction", 0, "define reduction", NULL);
+    psMetadataAddTime(definexccalrunArgs, PS_LIST_TAIL, "-set_registered", 0, "time detrend run was registered", now);
+    psMetadataAddBool(definexccalrunArgs, PS_LIST_TAIL, "-rerun",  0, "queue new run even if one already exists for inputs", false);
+    psMetadataAddBool(definexccalrunArgs, PS_LIST_TAIL, "-pretend",  0, "do not actually modify the database", false);
+    psMetadataAddBool(definexccalrunArgs, PS_LIST_TAIL, "-check_inputs",  0, "list inputs, do not modify database", false);
+    psMetadataAddBool(definexccalrunArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+    psMetadataAddU64(definexccalrunArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+
+    // -updatexccalrun
+    psMetadata *updatexccalrunArgs = psMetadataAlloc();
+    psMetadataAddS64(updatexccalrunArgs, PS_LIST_TAIL, "-xccal_id", 0,    "search by xccal ID", 0);
+    psMetadataAddS64(updatexccalrunArgs, PS_LIST_TAIL, "-xcsky_id", 0,    "search by xcsky ID", 0);
+    psMetadataAddS64(updatexccalrunArgs, PS_LIST_TAIL, "-xcstack_id", 0,  "search by xcstack ID", 0);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-state", 0,       "search by state", NULL);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-skycell_id", 0,  "search by skycell_id", 0);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-label", 0,       "search by label", 0);
+    pxskycellAddArguments(updatexccalrunArgs);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-set_label", 0, "define new value for label", NULL);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-set_state", 0, "define new state", NULL);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-set_data_group", 0, "define new data_group", NULL);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-set_dist_group", 0, "define new dist_group", NULL);
+    psMetadataAddStr(updatexccalrunArgs, PS_LIST_TAIL, "-set_note", 0, "define new note", NULL);
+
+    // -pendingxccalrun
+    psMetadata *pendingxccalrunArgs = psMetadataAlloc();
+    psMetadataAddS64(pendingxccalrunArgs, PS_LIST_TAIL, "-xccal_id", 0, "search by xccal ID", 0);
+    psMetadataAddS64(pendingxccalrunArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddStr(pendingxccalrunArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", NULL);
+    psMetadataAddStr(pendingxccalrunArgs, PS_LIST_TAIL, "-filter", 0, "search by filter", NULL);
+    psMetadataAddU64(pendingxccalrunArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(pendingxccalrunArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -addxccalresult
+    psMetadata *addxccalresultArgs = psMetadataAlloc();
+    psMetadataAddS64(addxccalresultArgs, PS_LIST_TAIL, "-xccal_id", 0,     "define xccal_id (required)", 0);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-sigma_ra", 0,     "define exposure E ra", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-sigma_dec", 0,    "define exposure E dec", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-zpt_obs", 0,      "define observed zero point", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-zpt_stdev", 0,    "define observed zero point stdandard deviation", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-fwhm_major", 0,   "define fwhm (major axis; pixels)", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-fwhm_minor", 0,   "define fwhm (minor axis; pixels)", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-dtime_script", 0, "define elapsed time in script (seconds)", NAN);
+    psMetadataAddF32(addxccalresultArgs, PS_LIST_TAIL, "-dtime_astrom", 0, "define elapsed time for astrometry (seconds)", NAN);
+
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-hostname", 0,            "define hostname", NULL);
+    psMetadataAddS32(addxccalresultArgs, PS_LIST_TAIL, "-n_astrom", 0,            "define number of astrometry reference objects", 0);
+    psMetadataAddS32(addxccalresultArgs, PS_LIST_TAIL, "-n_detections", 0,        "define number of detections", 0);
+    psMetadataAddS32(addxccalresultArgs, PS_LIST_TAIL, "-n_extended", 0,          "define number of extended detections", 0);
+    psMetadataAddS32(addxccalresultArgs, PS_LIST_TAIL, "-n_forced", 0,            "define number of forced detections", 0);
+
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-path_base", 0,            "define base output location", NULL);
+    psMetadataAddS16(addxccalresultArgs, PS_LIST_TAIL, "-fault",  0,            "set fault code", 0);
+    psMetadataAddS16(addxccalresultArgs, PS_LIST_TAIL, "-quality",  0,            "set quality", 0);
+
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-ver_pslib", 0, "define psLib version", NULL);
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-ver_psmodules", 0, "define psModules version", NULL);
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-ver_psphot", 0, "define psphot version", NULL);
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-ver_psastro", 0, "define psastro version", NULL);
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-ver_ppstats", 0, "define ppStats version", NULL);
+    psMetadataAddStr(addxccalresultArgs, PS_LIST_TAIL, "-ver_streaks", 0, "define streaksremove version", NULL);
+
+    // -revertxccal
+    psMetadata *revertxccalArgs= psMetadataAlloc();
+    psMetadataAddS64(revertxccalArgs, PS_LIST_TAIL,    "-xccal_id",   0, "search by xccal ID", 0);
+    psMetadataAddStr(revertxccalArgs, PS_LIST_TAIL,    "-label",      PS_META_DUPLICATE_OK, "search by label", 0);
+    psMetadataAddStr(revertxccalArgs, PS_LIST_TAIL,    "-data_group", PS_META_DUPLICATE_OK, "search by data_group", 0);
+    psMetadataAddStr(revertxccalArgs, PS_LIST_TAIL,    "-filter",     PS_META_DUPLICATE_OK, "search by filter", 0);
+    pxskycellAddArguments(revertxccalArgs);	       		     
+    psMetadataAddS16(revertxccalArgs, PS_LIST_TAIL,    "-fault",      0, "search by fault code", 0);
+    psMetadataAddBool(revertxccalArgs, PS_LIST_TAIL,   "-all",        0, "allow no search terms", false);
+
+    // -updatexccalresult
+    psMetadata *updatexccalresultArgs = psMetadataAlloc();
+    psMetadataAddS64(updatexccalresultArgs, PS_LIST_TAIL, "-xccal_id", 0, "define xccal ID (required)", 0);
+    psMetadataAddS16(updatexccalresultArgs, PS_LIST_TAIL, "-set_fault", 0, "set fault code (required)", 0);
+    psMetadataAddS16(updatexccalresultArgs, PS_LIST_TAIL, "-set_quality", 0, "set quality code", 0);
+
+    // -xccalresult
+    psMetadata *xccalresultArgs= psMetadataAlloc();
+    psMetadataAddS64(xccalresultArgs, PS_LIST_TAIL, "-xccal_id", 0, "search by xccal ID", 0);
+    psMetadataAddS64(xccalresultArgs, PS_LIST_TAIL, "-xcsky_id", 0, "search by xcsky ID", 0);
+    psMetadataAddS64(xccalresultArgs, PS_LIST_TAIL, "-xcstack_id", 0, "search by xcstack ID", 0);
+    psMetadataAddStr(xccalresultArgs, PS_LIST_TAIL, "-tess_id", 0, "search by tess ID (LIKE comparison)", 0);
+    psMetadataAddStr(xccalresultArgs, PS_LIST_TAIL, "-skycell_id", 0, "search by skycell ID (LIKE comparison)", 0);
+    psMetadataAddStr(xccalresultArgs, PS_LIST_TAIL, "-filter", 0, "search by filter (LIKE comparison)", 0);
+    psMetadataAddStr(xccalresultArgs, PS_LIST_TAIL, "-label", 0, "search by label (LIKE comparison)", NULL);
+    psMetadataAddStr(xccalresultArgs, PS_LIST_TAIL, "-data_group", 0, "search by data_group (LIKE comparison)", NULL);
+    psMetadataAddS16(xccalresultArgs, PS_LIST_TAIL, "-quality",  0, "search by quality", 0);
+    psMetadataAddS16(xccalresultArgs, PS_LIST_TAIL, "-fault", 0, "search by fault code", 0);
+    pxskycellAddArguments(xccalresultArgs);
+    psMetadataAddU64(xccalresultArgs, PS_LIST_TAIL, "-limit", 0, "limit xccalresult set to N items", 0);
+    psMetadataAddBool(xccalresultArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -exportxccalrun
+    psMetadata *exportxccalrunArgs = psMetadataAlloc();
+    psMetadataAddS64(exportxccalrunArgs, PS_LIST_TAIL, "-xccal_id", 0, "export this xccal ID (required)", 0);
+    psMetadataAddStr(exportxccalrunArgs, PS_LIST_TAIL, "-outfile", 0, "export to this file (required)", NULL);
+    psMetadataAddU64(exportxccalrunArgs, PS_LIST_TAIL, "-limit", 0, "limit result set to N items", 0);
+    psMetadataAddBool(exportxccalrunArgs, PS_LIST_TAIL, "-clean", 0, "mark tables as cleaned", false);
+
+    // -importrun
+    psMetadata *importxccalrunArgs = psMetadataAlloc();
+    psMetadataAddStr(importxccalrunArgs, PS_LIST_TAIL, "-infile", 0, "import from this file (required)", NULL);
+
+    psFree(now);
+
+    psMetadata *argSets = psMetadataAlloc();
+    psMetadata *modes   = psMetadataAlloc();
+
+    PXOPT_ADD_MODE("-definebyquery",   "Define a new run",         XCSKYTOOL_MODE_DEFINEBYQUERY,     definebyqueryArgs);
+    PXOPT_ADD_MODE("-updaterun",       "Update a run",             XCSKYTOOL_MODE_UPDATERUN,         updaterunArgs);
+    PXOPT_ADD_MODE("-inputs",          "Get inputs",               XCSKYTOOL_MODE_INPUTS,            inputsArgs);
+    PXOPT_ADD_MODE("-todo",            "Get runs to do",           XCSKYTOOL_MODE_TODO,              todoArgs);
+    PXOPT_ADD_MODE("-addresult",       "Add result of run",        XCSKYTOOL_MODE_ADDRESULT,         addresultArgs);
+    PXOPT_ADD_MODE("-result",          "Get result of run",        XCSKYTOOL_MODE_RESULT,            resultArgs);
+    PXOPT_ADD_MODE("-revert",          "Revert failed run",        XCSKYTOOL_MODE_REVERT,            revertArgs);
+    PXOPT_ADD_MODE("-updateresult",    "Update fault for run",     XCSKYTOOL_MODE_UPDATERESULT,      updateresultArgs);
+    PXOPT_ADD_MODE("-exportrun",       "Export run",               XCSKYTOOL_MODE_EXPORTRUN,         exportrunArgs);
+    PXOPT_ADD_MODE("-importrun",       "Import run",               XCSKYTOOL_MODE_IMPORTRUN,         importrunArgs);
+    PXOPT_ADD_MODE("-definexccalrun",  "Define a new xccalrun",    XCSKYTOOL_MODE_DEFINEXCCALRUN,    definexccalrunArgs);
+    PXOPT_ADD_MODE("-updatexccalrun",  "Update a xccalrun",        XCSKYTOOL_MODE_UPDATEXCCALRUN,    updatexccalrunArgs);
+    PXOPT_ADD_MODE("-pendingxccalrun", "Get xccal runs to do",     XCSKYTOOL_MODE_PENDINGXCCALRUN,   pendingxccalrunArgs);
+    PXOPT_ADD_MODE("-addxccalresult",  "add xccal result",         XCSKYTOOL_MODE_ADDXCCALRESULT,    addxccalresultArgs);
+    PXOPT_ADD_MODE("-revertxccal",     "revert faulted xccal run", XCSKYTOOL_MODE_REVERTXCCALRESULT, revertxccalArgs);
+    PXOPT_ADD_MODE("-updatexccal",     "revert faulted xccal run", XCSKYTOOL_MODE_UPDATEXCCALRESULT, updatexccalresultArgs);
+    PXOPT_ADD_MODE("-xccalresult",     "Get result of xccal run",  XCSKYTOOL_MODE_XCCALRESULT,       xccalresultArgs);
+    PXOPT_ADD_MODE("-exportxccalrun",  "Export xccal run",         XCSKYTOOL_MODE_EXPORTXCCALRUN,    exportxccalrunArgs);
+    PXOPT_ADD_MODE("-importxccalrun",  "Import xccal run",         XCSKYTOOL_MODE_IMPORTXCCALRUN,    importxccalrunArgs);
+
+    if (!pxGetOptions(stderr, argc, argv, config, modes, argSets)) {
+        psError(PS_ERR_UNKNOWN, true, "option parsing failed");
+        psFree(argSets);
+        psFree(modes);
+        psFree(config);
+        return NULL;
+    }
+
+    psFree(argSets);
+    psFree(modes);
+
+    // define Database handle, if used
+    // do this last so we don't setup a connection before CLI options are
+    // validated
+    config->dbh = psMemIncrRefCounter(pmConfigDB(config->modules));
+    if (!config->dbh) {
+        psError(PS_ERR_UNKNOWN, false, "Can't configure database");
+        psFree(config);
+        return NULL;
+    }
+
+    return config;
+}
Index: trunk/ippTools/src/xcstacktool.c
===================================================================
--- trunk/ippTools/src/xcstacktool.c	(revision 42837)
+++ trunk/ippTools/src/xcstacktool.c	(revision 42837)
@@ -0,0 +1,1544 @@
+/*
+ * xcstacktool.c
+ *
+ * Copyright (C) 2023 Eugene Magnier
+ * Based on stacktool.c (Joshua Hoblitt)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * program; see the file COPYING. If not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVB_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+#include <ippdb.h>
+
+#include "pxtools.h"
+#include "xcstacktool.h"
+
+static bool definebyqueryMode(pxConfig *config);
+static bool addbyqueryMode(pxConfig *config);
+static bool updaterunMode(pxConfig *config);
+static bool tosumMode(pxConfig *config);
+static bool inputcamerasMode(pxConfig *config);
+static bool inputskyfileMode(pxConfig *config);
+static bool addsumskyfileMode(pxConfig *config);
+static bool sumskyfileMode(pxConfig *config);
+static bool revertsumskyfileMode(pxConfig *config);
+static bool updatesumskyfileMode(pxConfig *config);
+static bool pendingcleanuprunMode(pxConfig *config);
+static bool pendingcleanupskyfileMode(pxConfig *config);
+static bool donecleanupMode(pxConfig *config);
+static bool exportrunMode(pxConfig *config);
+static bool importrunMode(pxConfig *config);
+static bool addcameraMode(pxConfig *config);
+static bool getcameraMode(pxConfig *config);
+static bool listcamerasMode(pxConfig *config);
+
+static bool setxcstackRunState(pxConfig *config, psS64 xcstack_id, const char *state);
+static psS64 getCameraIDfromName(pxConfig *config, const char *camera);
+
+# define MODECASE(caseName, func) \
+    case caseName: \
+    if (!func(config)) { \
+        goto FAIL; \
+    } \
+    break;
+
+int main(int argc, char **argv)
+{
+    psLibInit(NULL);
+
+    pxConfig *config = xcstacktoolConfig(NULL, argc, argv);
+    if (!config) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to configure");
+        goto FAIL;
+    }
+
+    switch (config->mode) {
+        MODECASE(XCSTACKTOOL_MODE_DEFINEBYQUERY,         definebyqueryMode);
+        MODECASE(XCSTACKTOOL_MODE_ADDBYQUERY,            addbyqueryMode);
+        MODECASE(XCSTACKTOOL_MODE_UPDATERUN,             updaterunMode);
+        MODECASE(XCSTACKTOOL_MODE_TOSUM,                 tosumMode);
+        MODECASE(XCSTACKTOOL_MODE_INPUTCAMERAS,          inputcamerasMode);
+        MODECASE(XCSTACKTOOL_MODE_INPUTSKYFILE,          inputskyfileMode);
+        MODECASE(XCSTACKTOOL_MODE_ADDSUMSKYFILE,         addsumskyfileMode);
+        MODECASE(XCSTACKTOOL_MODE_SUMSKYFILE,            sumskyfileMode);
+        MODECASE(XCSTACKTOOL_MODE_REVERTSUMSKYFILE,      revertsumskyfileMode);
+        MODECASE(XCSTACKTOOL_MODE_UPDATESUMSKYFILE,      updatesumskyfileMode);
+        MODECASE(XCSTACKTOOL_MODE_PENDINGCLEANUPRUN,     pendingcleanuprunMode);
+        MODECASE(XCSTACKTOOL_MODE_PENDINGCLEANUPSKYFILE, pendingcleanupskyfileMode);
+        MODECASE(XCSTACKTOOL_MODE_DONECLEANUP,           donecleanupMode);
+        MODECASE(XCSTACKTOOL_MODE_EXPORTRUN,             exportrunMode);
+        MODECASE(XCSTACKTOOL_MODE_IMPORTRUN,             importrunMode);
+        MODECASE(XCSTACKTOOL_MODE_ADDCAMERA,             addcameraMode);
+        MODECASE(XCSTACKTOOL_MODE_GETCAMERA,             getcameraMode);
+        MODECASE(XCSTACKTOOL_MODE_LISTCAMERAS,           listcamerasMode);
+        default:
+            psAbort("invalid option (this should not happen)");
+    }
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(EXIT_SUCCESS);
+
+FAIL:
+    psErrorStackPrint(stderr, "\n");
+    int exit_status = pxerrorGetExitStatus();
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(exit_status);
+}
+
+// xcstack is only choosing input stacks.  options can include: 
+// camera, filter, skycell, tess, label, data_group, 
+static bool definebyqueryMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required options
+    PXOPT_LOOKUP_STR(workdir, config->args, "-set_workdir", true, false);
+
+    // The camera must be supplied as a string, then the camera_id is selected
+    // from the table xcstackCamera.
+    PXOPT_LOOKUP_STR(camera, config->args, "-xcamera", true, false);
+
+    // get this camera_id from the database:
+    psS64 camera_id = getCameraIDfromName(config, camera);
+    if (!camera_id) {
+      psError(PXTOOLS_ERR_SYS, false, "failed to find camera in database");
+      return false;
+    }
+
+    // define options for the output runs
+    PXOPT_LOOKUP_STR(label, config->args, "-set_label", false, false);
+    PXOPT_LOOKUP_STR(data_group, config->args, "-set_data_group", false, false);
+    PXOPT_LOOKUP_STR(dist_group, config->args, "-set_dist_group", false, false);
+    PXOPT_LOOKUP_STR(reduction, config->args, "-set_reduction", false, false);
+    PXOPT_LOOKUP_STR(note, config->args, "-set_note", false, false);
+    PXOPT_LOOKUP_TIME(registered, config->args, "-set_registered", false, false);
+
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+    PXOPT_LOOKUP_BOOL(pretend, config->args, "-pretend", false);
+
+    // we want to be able to queue the same input stacks for multiple output xcstackRuns,
+    // but we do not want to duplicate entries which already exist for the same
+    // label & data_group (or just data_group??).
+
+    // the possible input stacks are matched to already existing xcstacks with the same
+    // label & data_group and only ones which do NOT match (xcstack_id is NULL) are queued.
+
+    // In order to speed the query operations, the possible matched xcstacks are selected
+    // based on the tess_id, skycell_id, data_group restrictions supplied
+
+    // SUBSET TABLE SQL statement:
+
+    psMetadata *where_XC = psMetadataAlloc();
+
+    // also restrict the possible matched xcstackRuns based on these options:
+    pxAddLabelSearchArgs(config,  where_XC, "-select_tess_id",         "xcstackRun.tess_id", "==");
+    pxAddLabelSearchArgs(config,  where_XC, "-select_skycell_id",      "xcstackRun.skycell_id", "LIKE");
+
+    // generate the xcstackRun clause
+    if (!psListLength(where_XC->list)) {
+      psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required (at least tess_id or skycell_id)");
+      psFree(where_XC);
+      return false;
+    }
+      
+    psString select_XC = pxDataGet("xcstacktool_definebyquery_select_XC.sql");
+    if (!select_XC) {
+      psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+      psFree(where_XC);
+      return false;
+    }
+      
+    psString whereClause_XC = psDBGenerateWhereConditionSQL(where_XC, NULL);
+    psStringAppend(&select_XC, " WHERE (xcstackInputSkyfile.camera_id = %d)\n AND %s", (int) camera_id, whereClause_XC);
+    psFree(whereClause_XC);
+
+    // add restrictions based on the output data_group and label
+    if (label)      { psStringAppend(&select_XC, "\n AND (xcstackRun.label      LIKE '%s')", label); }
+    if (data_group) { psStringAppend(&select_XC, "\n AND (xcstackRun.data_group LIKE '%s')", data_group); }
+
+    psFree(where_XC);
+
+    // MAIN SQL statement:
+
+    psMetadata *where = psMetadataAlloc();
+
+    // restrict the input stackRun based on these options:
+    pxAddLabelSearchArgs(config,  where, "-select_stack_id",           "stackRun.stack_id", "==");
+    pxAddLabelSearchArgs(config,  where, "-select_tess_id",            "stackRun.tess_id", "==");
+    pxAddLabelSearchArgs(config,  where, "-select_skycell_id",         "stackRun.skycell_id", "LIKE");
+
+    PXOPT_COPY_STR(config->args,  where, "-select_filter",             "stackRun.filter", "==");
+    pxAddLabelSearchArgs (config, where, "-select_label",              "stackRun.label", "LIKE");
+
+    PXOPT_COPY_F32(config->args,  where, "-select_good_frac_min",      "stackSumSkyfile.good_frac", ">=");
+    pxAddLabelSearchArgs(config,  where, "-select_data_group",         "stackRun.data_group", "LIKE");
+
+    if (!psListLength(where->list)) {
+      psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required (at least tess_id or skycell_id)");
+      psFree(where);
+      return false;
+    }
+
+    psString select = pxDataGet("xcstacktool_definebyquery_select.sql");
+    if (!select) {
+      psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+      psFree(where);
+      return false;
+    }
+
+    // merge base select and select_XC
+    psStringAppend (&select, "(%s)\n AS xcstackRunData\n ON (stackRun.stack_id = xcstackRunData.stack_id)\n WHERE xcstack_id IS NULL", select_XC);
+    psFree (select_XC);
+
+    psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+    psStringAppend(&select, "\n AND %s", whereClause);
+    psFree(whereClause);
+    psFree(where);
+
+    // camera name is prepended to stackRun and stackSumSkyfile tables in query
+    if (!p_psDBRunQueryF(config->dbh, select, camera, camera)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      psFree(select);
+      return false;
+    }
+    psFree(select);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+      psErrorCode err = psErrorCodeLast();
+      switch (err) {
+	case PS_ERR_DB_CLIENT:
+	  psError(PXTOOLS_ERR_SYS, false, "database error");
+	case PS_ERR_DB_SERVER:
+	  psError(PXTOOLS_ERR_PROG, false, "database error");
+	default:
+	  psError(PXTOOLS_ERR_PROG, false, "unknown error");
+      }
+      return false;
+    }
+    if (!psArrayLength(output)) {
+      psWarning("xcstacktool: no rows found");
+      psFree(output);
+      return true;
+    }
+    if (pretend) {
+      // negative simple so the default is true
+      if (!ippdbPrintMetadatas(stdout, output, "xcstackSkycells", !simple)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	psFree(output);
+	return false;
+      }
+      psFree(output);
+      return true;
+    }
+
+    if (!psDBTransaction(config->dbh)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      psFree(where);
+      return false;
+    }
+
+    // used only if we catch an error
+    psString error_message = NULL;
+
+    // the query above selects stacks to be added to the xcstackRun
+    // for each stack, we need camera_id, stack_id, skycell_id, tess_id
+    psArray *list = psArrayAllocEmpty(16); // List of runs, to print
+    for (long i = 0; i < output->n; i++) {
+      psMetadata *row = output->data[i]; // Row from select
+      bool status;
+
+      bool got_error = false;
+
+      // pull out the skycell_id, tess_id, filter
+      psString skycell_id = psMetadataLookupStr(&status, row, "skycell_id");
+      if (!status) {
+	psStringAppend(&error_message, "skycell_id ");
+	got_error = true;
+      }
+
+      psString tess_id = psMetadataLookupStr(&status, row, "tess_id");
+      if (!status) {
+	psStringAppend(&error_message, "tess_id ");
+	got_error = true;
+      }
+
+      psString filter = psMetadataLookupStr(&status, row, "filter");
+      if (!status) {
+	psStringAppend(&error_message, "filter ");
+	got_error = true;
+      } 
+
+      psS64 stack_id = psMetadataLookupS64(&status, row, "stack_id");
+      if (!status) {
+	psStringAppend(&error_message, "skycell_id ");
+	got_error = true;
+      }
+
+      if (got_error) {
+	psError(PS_ERR_UNKNOWN, false, "failed to find: %s", error_message);
+	psFree(output);
+	psFree(list);
+	psFree(error_message);
+	if (!psDBRollback(config->dbh)) {
+	  psError(PS_ERR_UNKNOWN, false, "database error");
+	}
+	return false;
+      }
+
+      // create a new stackRun for this stack
+      xcstackRunRow *run = xcstackRunRowAlloc(
+					      0,                          // ID
+					      "wait",                     // state
+					      workdir,
+					      label,
+					      data_group ? data_group : label,
+					      dist_group,
+					      reduction,
+					      NULL, // should be 'registered'
+					      skycell_id,
+					      tess_id,
+					      filter,
+					      NULL, // software_ver : supplied by xcstackRunRowAlloc
+					      note);
+
+      if (!xcstackRunInsertObject(config->dbh, run)) {
+	if (!psDBRollback(config->dbh)) {
+	  psError(PS_ERR_UNKNOWN, false, "database error");
+	}
+	psError(PS_ERR_UNKNOWN, false, "database error (cannot insert xcstackRun entry)");
+	psFree(output);
+	psFree(list);
+	psFree(run);
+	psFree(error_message);
+	return false;
+      }
+
+      // figure out the new stack_id
+      psS64 xcstack_id = psDBLastInsertID(config->dbh);
+      run->xcstack_id = xcstack_id;
+
+      psArrayAdd(list, list->n, run);
+      psFree(run);
+
+      // now insert the xcstackInputSkyfile entry (only one allowed?)
+      xcstackInputSkyfileRow *inrow = xcstackInputSkyfileRowAlloc(run->xcstack_id, camera_id, stack_id);
+
+      if (!xcstackInputSkyfileInsertObject(config->dbh, inrow)) {
+	if (!psDBRollback(config->dbh)) {
+	  psError(PS_ERR_UNKNOWN, false, "database error");
+	}
+	psError(PS_ERR_UNKNOWN, false, "database error (cannot insert xcstackRun entry)");
+	psFree(output);
+	psFree(list);
+	psFree(inrow);
+	psFree(error_message);
+	return false;
+      }
+      psFree (inrow);
+    }
+    psFree(error_message);
+    psFree(output);
+
+    if (!psDBCommit(config->dbh)) {
+      psError(PS_ERR_UNKNOWN, false, "database error");
+      psFree(list);
+      return false;
+    }
+
+    if (!xcstackRunPrintObjects(stdout, list, !simple)) {
+      psError(PS_ERR_UNKNOWN, false, "failed to print object");
+      psFree(list);
+      return false;
+    }
+    psFree(list);
+    return true;
+}
+
+// This option selects stacks from the specified camera which match existing stacks listed
+// in xcstackRun (based on tess_id, skycell_id, filter) and identifies ones for which a
+// stack run for this camera is NOT listed in xcstackInputSkyfile. Since the filter names
+// for cameras may not match, the filter for the input camera and the xcstack filter must
+// both be specified
+static bool addbyqueryMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // The camera must be supplied as a string, then the camera_id is selected
+    // from the table xcstackCamera.
+    PXOPT_LOOKUP_STR(camera, config->args, "-xcamera", true, false);
+
+    // required options
+    PXOPT_LOOKUP_STR(infilter, config->args, "-select_instack_filter", true, false);
+    PXOPT_LOOKUP_STR(xcfilter, config->args, "-select_xcstack_filter", true, false);
+
+    // get this camera_id from the database:
+    psS64 camera_id = getCameraIDfromName(config, camera);
+    if (!camera_id) {
+	psError(PXTOOLS_ERR_SYS, false, "failed to find camera in database");
+	return false;
+    }
+
+    // output options (e.g., workdir, etc) for xcstack run are already defined so
+    // are not listed here
+
+    psMetadata *where = psMetadataAlloc();
+
+    // select based on properties of the input stacks
+    pxAddLabelSearchArgs (config, where, "-select_stack_id",           "stackRun.stack_id",   "==");
+    pxAddLabelSearchArgs (config, where, "-select_tess_id",            "stackRun.tess_id",    "LIKE");
+    pxAddLabelSearchArgs (config, where, "-select_skycell_id",         "stackRun.skycell_id", "LIKE");
+
+    // this forces at least one where condition to exist
+    PXOPT_COPY_F32(config->args,  where, "-select_good_frac_min",      "stackSumSkyfile.good_frac", ">=");
+
+    pxAddLabelSearchArgs (config, where, "-select_instack_label",      "stackRun.label",      "LIKE");
+    pxAddLabelSearchArgs (config, where, "-select_xcstack_label",      "xcstackRun.label",    "LIKE");
+
+    pxAddLabelSearchArgs(config,  where, "-select_instack_data_group", "stackRun.data_group", "LIKE");
+    pxAddLabelSearchArgs(config,  where, "-select_xcstack_data_group", "xcstackRun.data_group", "LIKE");
+    
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+    PXOPT_LOOKUP_BOOL(pretend, config->args, "-pretend", false);
+
+    if (!psListLength(where->list)) {
+	psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+	psFree(where);
+	return false;
+    }
+
+    psString select = pxDataGet("xcstacktool_addbyquery_select.sql");
+    if (!select) {
+	psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+	psFree(where);
+	return false;
+    }
+
+    // we are always guaranteed to have a where condition because of the -select_good_frac_min above
+    psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+    psFree(where);
+
+    if (!p_psDBRunQueryF(config->dbh, select, camera, camera, infilter, xcfilter, whereClause, (int) camera_id)) {
+	psError(PS_ERR_UNKNOWN, false, "database error");
+	psFree(select);
+	psFree(whereClause);
+	return false;
+    }
+    psFree(whereClause);
+    psFree(select);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+	  case PS_ERR_DB_CLIENT:
+	    psError(PXTOOLS_ERR_SYS, false, "database error");
+	  case PS_ERR_DB_SERVER:
+	    psError(PXTOOLS_ERR_PROG, false, "database error");
+	  default:
+	    psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psWarning("xcstacktool: no rows found");
+        psFree(output);
+        return true;
+    }
+    if (pretend) {
+        // negative simple so the default is true
+        if (!ippdbPrintMetadatas(stdout, output, "xcstackSkycells", !simple)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to print array");
+            psFree(output);
+            return false;
+        }
+        psFree(output);
+        return true;
+    }
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(where);
+        return false;
+    }
+
+    // used only if we catch an error
+    psString error_message = NULL;
+
+    // the query above selects stacks to be added to the xcstackRun
+    // for each stack, we need camera_id, stack_id, skycell_id, tess_id
+    for (long i = 0; i < output->n; i++) {
+        psMetadata *row = output->data[i]; // Row from select
+        bool status;
+
+	bool got_error = false;
+
+        psS64 xcstack_id = psMetadataLookupS64(&status, row, "xcstack_id");
+        if (!status) {
+	    psStringAppend(&error_message, "xcstack_id ");
+	    got_error = true;
+        }
+
+        psS64 stack_id = psMetadataLookupS64(&status, row, "stack_id");
+        if (!status) {
+	    psStringAppend(&error_message, "stack_id ");
+	    got_error = true;
+        }
+
+	if (got_error) {
+            psError(PS_ERR_UNKNOWN, false, "failed to find: %s", error_message);
+            psFree(output);
+            psFree(error_message);
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            return false;
+        }
+
+	// now insert the xcstackInputSkyfile entry (only one allowed?)
+	xcstackInputSkyfileRow *inrow = xcstackInputSkyfileRowAlloc(xcstack_id, camera_id, stack_id);
+
+        if (!xcstackInputSkyfileInsertObject(config->dbh, inrow)) {
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            psError(PS_ERR_UNKNOWN, false, "database error (cannot insert xcstackRun entry)");
+            psFree(output);
+            psFree(inrow);
+            psFree(error_message);
+            return false;
+        }
+	psFree (inrow);
+    }
+    psFree(error_message);
+    psFree(output);
+
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+    return true;
+}
+
+static bool updaterunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id",  "xcstackRun.xcstack_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-label",       "xcstackRun.label",      "==");
+    PXOPT_COPY_STR(config->args, where, "-data_group",  "xcstackRun.data_group", "==");
+    PXOPT_COPY_STR(config->args, where, "-state",       "xcstackRun.state",      "==");
+
+    PXOPT_LOOKUP_S16(fault, config->args, "-fault", false, false);
+    
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    psString query = psStringCopy ("UPDATE xcstackRun");
+
+    if (fault) {
+        psStringAppend(&query, " JOIN xcstackSumSkyfile USING(xcstack_id)");
+        PXOPT_COPY_S16(config->args, where, "-fault", "xcstackSumSkyfile.fault", "==");
+    }
+    
+    // pxUpdateRun gets parameters from config->args and updates
+    bool result = pxUpdateRun(config, where, &query, "xcstackRun", "xcstack_id", "xcstackSumSkyfile", true, false);
+
+    psFree(query);
+    psFree(where);
+
+    return result;
+}
+
+static bool tosumMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id", "xcstack_id", "==");
+    pxAddLabelSearchArgs (config, where, "-label", "xcstackRun.label", "==");
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcstacktool_tosum.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    psString whereClause = psStringCopy(""); // WHERE conditions to add
+    if (psListLength(where->list)) {
+        psString new = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&whereClause, "\n AND %s", new);
+        psFree(new);
+    }
+    psFree(where);
+
+    psStringAppend(&query, "\nORDER by priority DESC, xcstack_id");
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQueryF(config->dbh, query, whereClause)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    // negative simple so the default is true
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackSumSkyfile", !simple)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	psFree(output);
+	return false;
+    }
+
+    psFree(output);
+    return true;
+}
+
+// return the input skycells needed to make this stack
+// unlike the single camera stacks, this mode is called
+// separately for each xcamera
+static bool inputcamerasMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // xcstack_id is required, need to add to the where explicitly
+    PXOPT_LOOKUP_S64(xcstack_id, config->args, "-xcstack_id", true, false);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = NULL;
+    psStringAppend (&query, "SELECT camera from xcstackInputSkyfile JOIN xcstackCamera USING (camera_id) WHERE (xcstack_id = %"PRId64")", xcstack_id);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS,  false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackInputCameras", !simple)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	psFree(output);
+	return false;
+    }
+
+    psFree(output);
+    return true;
+}
+
+// return the input skycells needed to make this stack
+// unlike the single camera stacks, this mode is called
+// separately for each xcamera
+static bool inputskyfileMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // xcstack_id is required, need to add to the where explicitly
+    PXOPT_LOOKUP_S64(xcstack_id, config->args, "-xcstack_id", true, false);
+
+    // The camera must be supplied as a string, then the camera_id is selected
+    // from the table xcstackCamera.
+    PXOPT_LOOKUP_STR(camera,    config->args, "-xcamera", true, false);  
+
+    // get this camera_id from the database:
+    psS64 camera_id = getCameraIDfromName(config, camera);
+    if (!camera_id) {
+	psError(PXTOOLS_ERR_SYS, false, "failed to find camera in database");
+	return false;
+    }
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcstacktool_inputskyfile.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQueryF(config->dbh, query, camera, camera_id, xcstack_id)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS,  false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackInputSkyfile", !simple)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	psFree(output);
+	return false;
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool addsumskyfileMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required
+    PXOPT_LOOKUP_S64(xcstack_id,    config->args, "-xcstack_id",    true, false);
+
+    // optional
+    PXOPT_LOOKUP_STR(path_base,     config->args, "-path_base",     false, false);
+    PXOPT_LOOKUP_F64(bg,            config->args, "-bg",            false, false);
+    PXOPT_LOOKUP_F64(bg_stdev,      config->args, "-bg_stdev",      false, false);
+    PXOPT_LOOKUP_F32(dtime_stack,   config->args, "-dtime_stack",   false, false);
+    PXOPT_LOOKUP_F32(dtime_script,  config->args, "-dtime_script",  false, false);
+    PXOPT_LOOKUP_STR(hostname,      config->args, "-hostname",      false, false);
+    PXOPT_LOOKUP_F32(good_frac,     config->args, "-good_frac",     false, false);
+
+    PXOPT_LOOKUP_STR(ver_pslib,     config->args, "-ver_pslib",     false, false);
+    PXOPT_LOOKUP_STR(ver_psmodules, config->args, "-ver_psmodules", false, false);
+    PXOPT_LOOKUP_STR(ver_ppstats,   config->args, "-ver_ppstats",   false, false);
+    PXOPT_LOOKUP_STR(ver_ppstack,   config->args, "-ver_ppstack",   false, false);
+    
+    psString software_ver = NULL;
+    if ((ver_pslib)&&(ver_psmodules)) {
+      software_ver = pxMergeCodeVersions(ver_pslib,ver_psmodules);
+    }
+    if (ver_ppstats) {
+      software_ver = pxMergeCodeVersions(software_ver,ver_ppstats);
+    }
+    if (ver_ppstack) {
+      software_ver = pxMergeCodeVersions(software_ver,ver_ppstack);
+    }
+
+    // default values
+    PXOPT_LOOKUP_S16(fault,   config->args, "-fault",   false, false);
+    PXOPT_LOOKUP_S16(quality, config->args, "-quality", false, false);
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (!xcstackSumSkyfileInsert(config->dbh,
+				 xcstack_id,
+				 path_base,
+				 bg,
+				 bg_stdev,
+				 dtime_stack,
+				 dtime_script,
+				 hostname,
+				 good_frac,
+				 fault,
+				 software_ver,
+				 quality
+          )) {
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "database error");
+        }
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (fault == 0) {
+	// on success, set to full
+        if (!setxcstackRunState(config, xcstack_id, "full")) {
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "database error");
+            }
+            psError(PS_ERR_UNKNOWN, false, "failed to change xcstackRun's state");
+            return false;
+        }
+    }
+
+    // point of no return
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+
+static bool sumskyfileMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id", "xcstackRun.xcstack_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-state",      "xcstackRun.state", "==");
+    PXOPT_COPY_STR(config->args, where, "-tess_id",    "xcstackRun.tess_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-skycell_id", "xcstackRun.skycell_id", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-filter",     "xcstackRun.filter", "LIKE");
+    pxAddLabelSearchArgs(config, where, "-label",      "xcstackRun.label", "LIKE");
+    pxAddLabelSearchArgs(config, where, "-data_group", "xcstackRun.data_group", "LIKE");
+    PXOPT_COPY_S16(config->args, where, "-fault",      "xcstackSumSkyfile.fault", "==");
+
+    PXOPT_LOOKUP_BOOL(all, config->args, "-all", false);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("xcstacktool_sumskyfile.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " WHERE %s", whereClause);
+        psFree(whereClause);
+    } else if (!all) {
+        psError(PXTOOLS_ERR_CONFIG, true, "search parameters or -all are required");
+        return false;
+    }
+
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackSumSkyfile", !simple)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	psFree(output);
+	return false;
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool revertsumskyfileMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id", "xcstackSumSkyfile.xcstack_id", "==");
+    pxAddLabelSearchArgs(config, where, "-label",      "xcstackRun.label", "==");
+    PXOPT_COPY_S16(config->args, where, "-fault",      "xcstackSumSkyfile.fault", "==");
+
+    if (!psListLength(where->list)) {
+        psFree(where);
+        psError(PXTOOLS_ERR_CONFIG, false, "search parameters are required");
+        return false;
+    }
+
+    // Delete product
+    psString delete = pxDataGet("xcstacktool_revertsumskyfile_delete.sql");
+    if (!delete) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&delete, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, delete)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(delete);
+        psFree(where);
+        return false;
+    }
+    psFree(delete);
+
+    int numRows = psDBAffectedRows(config->dbh); // Number of row affected
+    psLogMsg("xcstacktool", PS_LOG_INFO, "Deleted %d rows", numRows);
+
+    psFree(where);
+    return true;
+}
+
+// XXX fault can be set to any value but quality cannot be set to 0
+static bool updatesumskyfileMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    PXOPT_LOOKUP_S16(fault,      config->args, "-fault",       true,  false);
+    PXOPT_LOOKUP_S64(xcstack_id, config->args, "-xcstack_id",  true,  false);
+    PXOPT_LOOKUP_S16(quality,    config->args, "-set_quality", false, false);
+
+    psMetadata *where = psMetadataAlloc();
+    psMetadataAddS64 (where, PS_LIST_TAIL, "xcstack_id", 0, "==", xcstack_id);
+
+    if (!pxSetFaultCode(config->dbh, "xcstackSumSkyfile", where, fault, quality)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to set set fault flag");
+        psFree (where);
+        return false;
+    }
+    psFree (where);
+
+    return true;
+}
+
+static bool pendingcleanuprunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psMetadata *where = psMetadataAlloc();
+    pxAddLabelSearchArgs (config, where, "-label", "xcstackRun.label", "==");
+    PXOPT_COPY_S64(config->args, where, "-xcstack_id", "xcstackRun.xcstack_id", "==");
+
+    psString query = pxDataGet("xcstacktool_pendingcleanuprun.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (where && psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    // negative simple so the default is true
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackPendingCleanupRun", !simple)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to print array");
+        psFree(output);
+        return false;
+    }
+
+    psFree(output);
+    return true;
+}
+
+static bool pendingcleanupskyfileMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_S64(xcstack_id, config->args, "-xcstack_id", true, false);
+    PXOPT_LOOKUP_U64(limit,      config->args, "-limit",      false, false);
+    PXOPT_LOOKUP_BOOL(simple,    config->args, "-simple",     false);
+
+    psMetadata *where = psMetadataAlloc();
+    psMetadataAddS64 (where, PS_LIST_TAIL, "xcstack_id", 0, "==", xcstack_id);
+    pxAddLabelSearchArgs (config, where, "-label", "xcstackRun.label", "==");
+
+    psString query = pxDataGet("xcstacktool_pendingcleanupskyfile.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    // should always be true: we require xcstack_id in the WHERE
+    if (where && psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    // negative simple so the default is true
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackPendingCleanupSkyfile", !simple)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to print array");
+        psFree(output);
+        return false;
+    }
+
+    psFree(output);
+
+    return true;
+}
+
+
+// XXX how is this mode used in practice?
+static bool donecleanupMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psMetadata *where = psMetadataAlloc();
+    PXOPT_COPY_STR(config->args, where, "-label", "label", "==");
+
+    psString query = pxDataGet("xcstacktool_donecleanup.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+        return false;
+    }
+
+    if (where && psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+    psFree(where);
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("xcstacktool", PS_LOG_INFO, "no rows found");
+        psFree(output);
+        return true;
+    }
+
+    // negative simple so the default is true
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackDoneCleanup", !simple)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to print array");
+        psFree(output);
+        return false;
+    }
+
+    psFree(output);
+
+    return true;
+}
+
+static bool exportrunMode(pxConfig *config)
+{
+    typedef struct ExportTable {
+	char tableName[80];
+	char tableSQL[80];
+    } ExportTable;
+
+    int numExportTables = 3;
+
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_S64(xcstack_id, config->args, "-xcstack_id", true,  false);
+    PXOPT_LOOKUP_STR(outfile, config->args, "-outfile", true,  false);
+    PXOPT_LOOKUP_BOOL(clean, config->args, "-clean", false);
+    PXOPT_LOOKUP_U64(limit,   config->args, "-limit",   false, false);
+
+    FILE *f = fopen (outfile, "w");
+    if (f == NULL) {
+	psError(PS_ERR_UNKNOWN, false, "failed to open output file");
+	return false;
+    }
+
+    if (!pxExportVersion(config, f)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to write dbversion output file");
+	return false;
+    }
+    psMetadata *where = psMetadataAlloc();
+    psMetadataAddS64 (where, PS_LIST_TAIL, "xcstack_id", 0, "==", xcstack_id);
+
+    ExportTable tables [] = {
+	{"xcstackRun",          "SELECT xcstackRun.*          FROM xcstackRun"},
+	{"xcstackInputSkyfile", "SELECT xcstackInputSkyfile.* FROM xcstackInputSkyfile"},
+	{"xcstackSumSkyfile",   "SELECT xcstackSumSkyfile.*   FROM xcstackSumSkyfile"},
+    };
+
+    for (int i = 0; i < numExportTables; i++) {
+	psString query = psStringCopy(tables[i].tableSQL);
+
+	if (where && psListLength(where->list)) {
+	    psString whereClause = psDBGenerateWhereSQL(where, NULL);
+	    psStringAppend(&query, " %s", whereClause);
+	    psFree(whereClause);
+	}
+
+	// treat limit == 0 as "no limit"
+	if (limit) {
+	    psString limitString = psDBGenerateLimitSQL(limit);
+	    psStringAppend(&query, " %s", limitString);
+	    psFree(limitString);
+	}
+
+	if (!p_psDBRunQuery(config->dbh, query)) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    psFree(query);
+	    return false;
+	}
+	psFree(query);
+
+	psArray *output = p_psDBFetchResult(config->dbh);
+	if (!output) {
+	    psError(PS_ERR_UNKNOWN, false, "database error");
+	    return false;
+	}
+	if (!psArrayLength(output)) {
+	    psError(PS_ERR_UNKNOWN, true, "no rows found");
+	    psFree(output);
+	    return false;
+	}
+
+	if (clean) {
+	    if (!strcmp(tables[i].tableName, "xcstackRun")) {
+		if (!pxSetStateCleaned("xcstackRun", "state", output)) {
+		    psFree(output);
+		    psError(PS_ERR_UNKNOWN, false, "pxSetStateClean failed for table %s",  tables[i].tableName);
+		    return false;
+		}
+	    }
+	}
+
+	// we must write the export table in non-simple (true) format
+	if (!ippdbPrintMetadatas(f, output, tables[i].tableName, true)) {
+	    psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	    psFree(output);
+	    return false;
+	}
+
+	psFree(output);
+    }
+
+    fclose (f);
+
+    return true;
+}
+
+static bool importrunMode(pxConfig *config)
+{
+  unsigned int nFail;
+
+  int numImportTables = 2;
+
+  char tables[2][80] = {"xcstackInputSkyfile", "xcstackSumSkyfile"};
+
+  PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+  PXOPT_LOOKUP_STR(infile, config->args, "-infile", true,  false);
+
+  psMetadata *input = psMetadataConfigRead (NULL, &nFail, infile, false);
+
+  if (!pxCheckImportVersion(config, input)) {
+      psError(PS_ERR_UNKNOWN, false, "pxCheckImportVersion failed");
+      return false;
+  }
+  psMetadataItem *item = psMetadataLookup (input, "xcstackRun");
+  psAssert (item, "entry not in input?");
+  psAssert (item->type == PS_DATA_METADATA_MULTI, "entry not multi?");
+
+  psMetadataItem *entry = psListGet (item->data.list, 0);
+  assert (entry);
+  assert (entry->type == PS_DATA_METADATA);
+  xcstackRunRow *xcstackRun = xcstackRunObjectFromMetadata (entry->data.md);
+  xcstackRunInsertObject (config->dbh, xcstackRun);
+
+  for (int i = 0; i < numImportTables; i++) {
+    psMetadataItem *item = psMetadataLookup (input, tables[i]);
+    psAssert (item, "entry not in input?");
+    psAssert (item->type == PS_DATA_METADATA_MULTI, "entry not multi?");
+
+    switch (i) {
+      case 0:
+        for (int i = 0; i < item->data.list->n; i++) {
+          entry = psListGet (item->data.list, i);
+          assert (entry);
+          assert (entry->type == PS_DATA_METADATA);
+          xcstackInputSkyfileRow *xcstackInputSkyfile = xcstackInputSkyfileObjectFromMetadata (entry->data.md);
+          xcstackInputSkyfileInsertObject (config->dbh, xcstackInputSkyfile);
+        }
+        break;
+
+      case 1:
+        for (int i = 0; i < item->data.list->n; i++) {
+          entry = psListGet (item->data.list, i);
+          assert (entry);
+          assert (entry->type == PS_DATA_METADATA);
+          xcstackSumSkyfileRow *xcstackSumSkyfile = xcstackSumSkyfileObjectFromMetadata (entry->data.md);
+          xcstackSumSkyfileInsertObject (config->dbh, xcstackSumSkyfile);
+        }
+        break;
+    }
+  }
+
+  return true;
+}
+
+static bool addcameraMode(pxConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_STR(camera, config->args, "-xcamera", true,  false);
+
+    psString query = pxDataGet("xcstacktool_addcamera.sql");
+    if (!query) {
+	psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+	return false;
+    }
+
+    psStringSubstitute(&query, camera, "@CAMERA@");
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+	psError(PS_ERR_UNKNOWN,false, "database error");
+	return(NULL);
+    }
+
+    int camera_id = psDBLastInsertID(config->dbh);
+    psLogMsg("stacktool", PS_LOG_INFO, "Added new camera %s (ID = %d)", camera, camera_id);
+
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return false;
+    }
+
+    return true;
+}
+
+static bool getcameraMode(pxConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_STR(camera, config->args, "-xcamera", true,  false);
+
+    psS64 camera_id = getCameraIDfromName(config, camera);
+
+    psLogMsg("stacktool", PS_LOG_INFO, "Camera %s = %d", camera, (int) camera_id);
+
+    return true;
+}
+
+static bool listcamerasMode(pxConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    char *query = "SELECT camera, camera_id FROM xcstackCamera";
+    if (!p_psDBRunQuery(config->dbh, query)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to select cameras");
+        return 0;
+    }
+    
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+	  case PS_ERR_DB_CLIENT:
+	    psError(PXTOOLS_ERR_SYS, false, "database error");
+	  case PS_ERR_DB_SERVER:
+	    psError(PXTOOLS_ERR_PROG, false, "database error");
+	  default:
+	    psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        return false;
+    }
+
+    // we expect a value camera_id as a result
+    if (!psArrayLength(output)) {
+        psWarning("xcstacktool: no cameras found");
+        psFree(output);
+        return false;
+    }
+    
+    if (!ippdbPrintMetadatas(stdout, output, "xcstackCamera", !simple)) {
+	psError(PS_ERR_UNKNOWN, false, "failed to print array");
+	psFree(output);
+	return false;
+    }
+
+# if (0)
+    // alternative
+    for (long i = 0; i < output->n; i++) {
+        psMetadata *row = output->data[i]; // Row from select
+        bool status;
+	
+	// pull out the skycell_id, tess_id, filter
+	psS64 camera_id = psMetadataLookupS64(&status, row, "camera_id");
+	if (!status) {
+	    psAbort("xcstacktool: camera_id not found in query result?");
+	    psFree(output);
+	    return false;
+	}
+	
+	// pull out the skycell_id, tess_id, filter
+	psString camera_name = psMetadataLookupStr(&status, row, "camera");
+	if (!status) {
+	    psAbort("xcstacktool: camera name not found in query result?");
+	    psFree(output);
+	    return false;
+	}
+	
+	psLogMsg("stacktool", PS_LOG_INFO, "Camera %s = %d", camera_name, (int) camera_id);
+    }
+# endif
+    return true;
+}
+
+static bool setxcstackRunState(pxConfig *config, psS64 xcstack_id, const char *state)
+{
+    PS_ASSERT_PTR_NON_NULL(state, false);
+
+    // check that state is a valid string value
+    if (!pxIsValidState(state)) {
+        psError(PS_ERR_UNKNOWN, false, "invalid xcstackRun state: %s", state);
+        return false;
+    }
+
+    char *query = "UPDATE xcstackRun SET state = '%s' WHERE xcstack_id = %"PRId64;
+    if (!p_psDBRunQueryF(config->dbh, query, state, xcstack_id)) {
+        psError(PS_ERR_UNKNOWN, false,
+                "failed to change state for xcstack_id %"PRId64, xcstack_id);
+        return false;
+    }
+
+    return true;
+}
+
+// This function returns the camera_id associated with the camera name.
+// We expect the camera to be defined, so an invalid name or a missing camera
+// results in a 0 return value which should be treated as an error.
+static psS64 getCameraIDfromName(pxConfig *config, const char *camera)
+{
+    PS_ASSERT_PTR_NON_NULL(camera, false);
+
+    char *query = "SELECT camera_id FROM xcstackCamera WHERE camera = '%s'";
+    if (!p_psDBRunQueryF(config->dbh, query, camera)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to select camera_id for camera %s", camera);
+        return 0;
+    }
+
+    psArray *output = p_psDBFetchResult(config->dbh);
+    if (!output) {
+        psErrorCode err = psErrorCodeLast();
+        switch (err) {
+            case PS_ERR_DB_CLIENT:
+                psError(PXTOOLS_ERR_SYS, false, "database error");
+            case PS_ERR_DB_SERVER:
+                psError(PXTOOLS_ERR_PROG, false, "database error");
+            default:
+                psError(PXTOOLS_ERR_PROG, false, "unknown error");
+        }
+        return false;
+    }
+
+    // we expect a value camera_id as a result
+    if (!psArrayLength(output)) {
+        psWarning("xcstacktool: no rows found");
+        psFree(output);
+        return false;
+    }
+
+    // we expect a single camera_id as a result
+    if (psArrayLength(output) > 1) {
+        psWarning("xcstacktool: too many camera?");
+        psFree(output);
+        return false;
+    }
+
+    // the query above selects stacks to be added to the xcstackRun
+    // for each stack, we need camera_id, stack_id, skycell_id, tess_id
+    psMetadata *row = output->data[0]; // Row from select
+    bool status;
+
+    // pull out the skycell_id, tess_id, filter
+    psS64 camera_id = psMetadataLookupS64(&status, row, "camera_id");
+    if (!status) {
+        psAbort("xcstacktool: camera_id not found in query result?");
+        psFree(output);
+        return false;
+    }
+
+    return camera_id;
+}
+
+
Index: trunk/ippTools/src/xcstacktool.h
===================================================================
--- trunk/ippTools/src/xcstacktool.h	(revision 42837)
+++ trunk/ippTools/src/xcstacktool.h	(revision 42837)
@@ -0,0 +1,49 @@
+/*
+ * xcstacktool.h
+ *
+ * Copyright (C) 2007  Joshua Hoblitt
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * program; see the file COPYING. If not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef XCSTACKTOOL_H
+#define XCSTACKTOOL_H 1
+
+#include "pxtools.h"
+
+typedef enum {
+    XCSTACKTOOL_MODE_NONE           = 0x0,
+    XCSTACKTOOL_MODE_DEFINEBYQUERY,
+    XCSTACKTOOL_MODE_ADDBYQUERY,
+    XCSTACKTOOL_MODE_UPDATERUN,
+    XCSTACKTOOL_MODE_TOSUM,
+    XCSTACKTOOL_MODE_INPUTCAMERAS,
+    XCSTACKTOOL_MODE_INPUTSKYFILE,
+    XCSTACKTOOL_MODE_ADDSUMSKYFILE,
+    XCSTACKTOOL_MODE_SUMSKYFILE,
+    XCSTACKTOOL_MODE_REVERTSUMSKYFILE,
+    XCSTACKTOOL_MODE_PENDINGCLEANUPRUN,
+    XCSTACKTOOL_MODE_PENDINGCLEANUPSKYFILE,
+    XCSTACKTOOL_MODE_DONECLEANUP,
+    XCSTACKTOOL_MODE_UPDATESUMSKYFILE,
+    XCSTACKTOOL_MODE_EXPORTRUN,
+    XCSTACKTOOL_MODE_IMPORTRUN,
+    XCSTACKTOOL_MODE_ADDCAMERA,
+    XCSTACKTOOL_MODE_GETCAMERA,
+    XCSTACKTOOL_MODE_LISTCAMERAS
+} xcstacktoolMode;
+
+pxConfig *xcstacktoolConfig(pxConfig *config, int argc, char **argv);
+
+#endif // XCSTACKTOOL_H
Index: trunk/ippTools/src/xcstacktoolConfig.c
===================================================================
--- trunk/ippTools/src/xcstacktoolConfig.c	(revision 42837)
+++ trunk/ippTools/src/xcstacktoolConfig.c	(revision 42837)
@@ -0,0 +1,259 @@
+/*
+ * xcstacktoolConfig.c
+ *
+ * Copyright (C) 2007-2008  Joshua Hoblitt
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * program; see the file COPYING. If not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <psmodules.h>
+
+#include "pxtools.h"
+#include "xcstacktool.h"
+
+pxConfig *xcstacktoolConfig(pxConfig *config, int argc, char **argv)
+{
+    if (!config) {
+        config = pxConfigAlloc();
+    }
+
+    pmConfigReadParamsSet(false);
+
+    // setup site config
+    config->modules = pmConfigRead(&argc, argv, NULL);
+    if (!config->modules) {
+        psError(psErrorCodeLast(), false, "Can't find site configuration");
+        psFree(config);
+        return NULL;
+    }
+
+    psTime *now = psTimeGetNow(PS_TIME_TAI);
+
+    // -definebyquery
+    psMetadata *definebyqueryArgs = psMetadataAlloc();
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-xcamera",           0, "camera source of input stacks", 0);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_workdir",       0, "define workdir (required)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_label",         0, "define label", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_data_group",    0, "define data group", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_dist_group",    0, "define dist group", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_note",          0, "define note", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-set_reduction",     0, "define reduction", NULL);
+    psMetadataAddTime(definebyqueryArgs, PS_LIST_TAIL, "-set_registered",    0, "time detrend run was registered", now);
+    psMetadataAddS64(definebyqueryArgs,  PS_LIST_TAIL, "-select_stack_id",   PS_META_DUPLICATE_OK, "search for stack_id", 0);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_tess_id",    PS_META_DUPLICATE_OK, "search for tess_id", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_skycell_id", PS_META_DUPLICATE_OK, "search for skycell_id (LIKE comparison, multiple ok)", NULL);
+    psMetadataAddF32(definebyqueryArgs,  PS_LIST_TAIL, "-select_good_frac_min", 0, "define min good_frac", 0.0);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_filter",     0, "search by filter", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_label",      PS_META_DUPLICATE_OK, "search by warpRun label (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(definebyqueryArgs,  PS_LIST_TAIL, "-select_data_group", PS_META_DUPLICATE_OK , "search by warpRun data_group (LIKE comparison, multiple ok)", NULL);
+
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-pretend",  0, "do not actually modify the database", false);
+    psMetadataAddBool(definebyqueryArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -addbyquery
+    psMetadata *addbyqueryArgs = psMetadataAlloc();
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-xcamera",                   0, "camera source of input stacks", 0);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_instack_filter",     0, "search by input stack filter", NULL);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_xcstack_filter",     0, "search by xcstack filter", NULL);
+    psMetadataAddS64(addbyqueryArgs,  PS_LIST_TAIL, "-select_stack_id",           PS_META_DUPLICATE_OK, "search for stack_id", 0);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_tess_id",            PS_META_DUPLICATE_OK, "search for tess_id", NULL);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_skycell_id",         PS_META_DUPLICATE_OK, "search for skycell_id (LIKE comparison, multiple ok)", NULL);
+    psMetadataAddF32(addbyqueryArgs,  PS_LIST_TAIL, "-select_good_frac_min",      0, "define min good_frac", 0.0);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_instack_label",      PS_META_DUPLICATE_OK, "search by label for input stack (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_xcstack_label",      PS_META_DUPLICATE_OK, "search by label for xcstack (LIKE comparison, multiple OK)", NULL);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_instack_data_group", PS_META_DUPLICATE_OK , "search by data_group of input stack (LIKE comparison, multiple ok)", NULL);
+    psMetadataAddStr(addbyqueryArgs,  PS_LIST_TAIL, "-select_xcstack_data_group", PS_META_DUPLICATE_OK , "search by data_group of xcstack (LIKE comparison, multiple ok)", NULL);
+    psMetadataAddBool(addbyqueryArgs, PS_LIST_TAIL, "-pretend",  0, "do not actually modify the database", false);
+    psMetadataAddBool(addbyqueryArgs, PS_LIST_TAIL, "-simple", 0, "use the simple output format", false);
+
+    // -updaterun
+    psMetadata *updaterunArgs = psMetadataAlloc();
+    psMetadataAddS64(updaterunArgs, PS_LIST_TAIL, "-xcstack_id", 0,         "search by stack ID", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-label", 0,            "search by label", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-data_group", 0,       "search by data_group", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-state", 0,            "search by state", NULL);
+    psMetadataAddS16(updaterunArgs, PS_LIST_TAIL, "-fault",  0,           "search by fault code", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_label", 0,        "define new value for label", 0);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_state", 0,        "define new state", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_data_group", 0,   "define new data_group", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_dist_group", 0,   "define new dist_group", NULL);
+    psMetadataAddStr(updaterunArgs, PS_LIST_TAIL, "-set_note", 0,         "define new note", NULL);
+
+    psMetadataAddBool(updaterunArgs, PS_LIST_TAIL, "-pretend",  0, "show query but do not actually modify the database", false);
+
+    // -tosum
+    psMetadata *tosumArgs = psMetadataAlloc();
+    psMetadataAddS64(tosumArgs, PS_LIST_TAIL, "-xcstack_id", 0,            "search by xcstack ID", 0);
+    psMetadataAddStr(tosumArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", NULL);
+    psMetadataAddU64(tosumArgs, PS_LIST_TAIL, "-limit",  0,            "limit result set to N items", 0);
+    psMetadataAddBool(tosumArgs, PS_LIST_TAIL, "-simple",  0,            "use the simple output format", false);
+
+    // -inputcameras
+    psMetadata *inputcamerasArgs = psMetadataAlloc();
+    psMetadataAddS64 (inputcamerasArgs, PS_LIST_TAIL, "-xcstack_id", 0,        "search by xcstack ID (required)", 0);
+    psMetadataAddU64 (inputcamerasArgs, PS_LIST_TAIL, "-limit",  0,            "limit result set to N items", 0);
+    psMetadataAddBool(inputcamerasArgs, PS_LIST_TAIL, "-simple",  0,            "use the simple output format", false);
+
+    // -inputskyfile: xcstack_id and camera_id are both required
+    psMetadata *inputskyfileArgs = psMetadataAlloc();
+    psMetadataAddS64(inputskyfileArgs, PS_LIST_TAIL, "-xcstack_id", 0,        "search by xcstack ID (required)", 0);
+    psMetadataAddStr(inputskyfileArgs, PS_LIST_TAIL, "-xcamera", 0,           "camera database to search (required)", 0);
+    psMetadataAddU64(inputskyfileArgs, PS_LIST_TAIL, "-limit",  0,            "limit result set to N items", 0);
+    psMetadataAddBool(inputskyfileArgs, PS_LIST_TAIL, "-simple",  0,          "use the simple output format", false);
+
+    // -addsumskyfile
+    psMetadata *addsumskyfileArgs = psMetadataAlloc();
+    psMetadataAddS64(addsumskyfileArgs, PS_LIST_TAIL, "-xcstack_id", 0,            "define stack ID (required)", 0);
+    psMetadataAddStr(addsumskyfileArgs, PS_LIST_TAIL, "-path_base", 0,            "define base output location", 0);
+    psMetadataAddF64(addsumskyfileArgs, PS_LIST_TAIL, "-bg",  0,            "define exposue background", NAN);
+    psMetadataAddF64(addsumskyfileArgs, PS_LIST_TAIL, "-bg_stdev",  0,            "define exposue background mean stdev", NAN);
+    psMetadataAddF32(addsumskyfileArgs, PS_LIST_TAIL, "-dtime_stack",  0,            "define elapsed processing time", NAN);
+    psMetadataAddF32(addsumskyfileArgs, PS_LIST_TAIL, "-dtime_script", 0, "define elapsed time in script (seconds)", NAN);
+
+    psMetadataAddStr(addsumskyfileArgs, PS_LIST_TAIL, "-hostname", 0,            "define hostname", 0);
+    psMetadataAddF32(addsumskyfileArgs, PS_LIST_TAIL, "-good_frac",  0,            "define %% of good pixels", NAN);
+    psMetadataAddS16(addsumskyfileArgs, PS_LIST_TAIL, "-fault",  0,            "set fault code", 0);
+    psMetadataAddS16(addsumskyfileArgs, PS_LIST_TAIL, "-quality",  0,            "set quality", 0);
+
+    psMetadataAddStr(addsumskyfileArgs, PS_LIST_TAIL, "-ver_pslib", 0, "define psLib version", NULL);
+    psMetadataAddStr(addsumskyfileArgs, PS_LIST_TAIL, "-ver_psmodules", 0, "define psModules version", NULL);
+    psMetadataAddStr(addsumskyfileArgs, PS_LIST_TAIL, "-ver_ppstats", 0, "define ppStats version", NULL);
+    psMetadataAddStr(addsumskyfileArgs, PS_LIST_TAIL, "-ver_ppstack", 0, "define ppStack version", NULL);
+
+    // -sumskyfile
+    psMetadata *sumskyfileArgs= psMetadataAlloc();
+    psMetadataAddS64(sumskyfileArgs, PS_LIST_TAIL, "-xcstack_id", 0,                    "search by xcstack ID", 0);
+    psMetadataAddStr(sumskyfileArgs, PS_LIST_TAIL, "-state", 0,                         "search by state", NULL);
+    psMetadataAddStr(sumskyfileArgs, PS_LIST_TAIL, "-tess_id", 0,                       "search by tess ID (LIKE comparison)", 0);
+    psMetadataAddStr(sumskyfileArgs, PS_LIST_TAIL, "-skycell_id", 0,                    "search by skycell ID (LIKE comparison)", 0);
+    psMetadataAddStr(sumskyfileArgs, PS_LIST_TAIL, "-filter", 0,                        "search by filter (LIKE comparison)", NULL);
+    psMetadataAddStr(sumskyfileArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK,      "search by label", NULL);
+    psMetadataAddStr(sumskyfileArgs, PS_LIST_TAIL, "-data_group", PS_META_DUPLICATE_OK, "search by data_group (LIKE comparison)", NULL);
+    psMetadataAddS16(sumskyfileArgs, PS_LIST_TAIL, "-fault",  0,                        "search by fault code", 0);
+    psMetadataAddU64(sumskyfileArgs, PS_LIST_TAIL, "-limit",  0,                        "limit result set to N items", 0);
+    psMetadataAddBool(sumskyfileArgs, PS_LIST_TAIL, "-simple",  0,                      "use the simple output format", false);
+    psMetadataAddBool(sumskyfileArgs, PS_LIST_TAIL, "-all",  0,                         "enable search without arguments", false);
+
+    // -revertsumskyfile
+    psMetadata *revertsumskyfileArgs= psMetadataAlloc();
+    psMetadataAddS64(revertsumskyfileArgs, PS_LIST_TAIL, "-xcstack_id", 0,            "search by xcstack ID", 0);
+    psMetadataAddStr(revertsumskyfileArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "search by label", 0);
+    psMetadataAddS16(revertsumskyfileArgs, PS_LIST_TAIL, "-fault",  0,            "search by fault code", 0);
+    psMetadataAddBool(revertsumskyfileArgs, PS_LIST_TAIL, "-all",  0, "allow no search terms", 0);
+
+    // -updatesumskyfile
+    psMetadata *updatesumskyfileArgs = psMetadataAlloc();
+    psMetadataAddS64(updatesumskyfileArgs, PS_LIST_TAIL, "-xcstack_id", 0,            "define xcstack ID (required)", 0);
+    psMetadataAddS16(updatesumskyfileArgs, PS_LIST_TAIL, "-fault", 0,                 "set fault code (required)", 0);
+    psMetadataAddS16(updatesumskyfileArgs, PS_LIST_TAIL, "-set_quality", 0,           "set quality", 0);
+    
+    // -pendingcleanuprun
+    psMetadata *pendingcleanuprunArgs = psMetadataAlloc();
+    psMetadataAddS64(pendingcleanuprunArgs, PS_LIST_TAIL, "-xcstack_id", 0,          "search by xcstack ID", 0);
+    psMetadataAddStr(pendingcleanuprunArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "list blocks for specified label", NULL);
+    psMetadataAddBool(pendingcleanuprunArgs, PS_LIST_TAIL, "-simple",  0,            "use the simple output format", false);
+    psMetadataAddU64(pendingcleanuprunArgs, PS_LIST_TAIL, "-limit",  0,            "limit result set to N items", 0);
+
+    // -pendingcleanupskyfile
+    psMetadata *pendingcleanupskyfileArgs = psMetadataAlloc();
+    psMetadataAddS64(pendingcleanupskyfileArgs, PS_LIST_TAIL, "-xcstack_id", 0,          "search by xcstack ID (required)", 0);
+    psMetadataAddStr(pendingcleanupskyfileArgs, PS_LIST_TAIL, "-label", PS_META_DUPLICATE_OK, "list blocks for specified label", NULL);
+    psMetadataAddBool(pendingcleanupskyfileArgs, PS_LIST_TAIL, "-simple",  0,            "use the simple output format", false);
+    psMetadataAddU64(pendingcleanupskyfileArgs, PS_LIST_TAIL, "-limit",  0,            "limit result set to N items", 0);
+
+    // -donecleanup
+    psMetadata *donecleanupArgs = psMetadataAlloc();
+    psMetadataAddStr(donecleanupArgs, PS_LIST_TAIL, "-label",  0,            "list blocks for specified label", NULL);
+    psMetadataAddBool(donecleanupArgs, PS_LIST_TAIL, "-simple",  0,            "use the simple output format", false);
+    psMetadataAddU64(donecleanupArgs, PS_LIST_TAIL, "-limit",  0,            "limit result set to N items", 0);
+
+    // -exportrun
+    psMetadata *exportrunArgs = psMetadataAlloc();
+    psMetadataAddS64(exportrunArgs, PS_LIST_TAIL, "-xcstack_id", 0,       "export this xcstack ID (required)", 0);
+    psMetadataAddStr(exportrunArgs, PS_LIST_TAIL, "-outfile", 0,          "export to this file (required)", NULL);
+    psMetadataAddU64(exportrunArgs, PS_LIST_TAIL, "-limit",   0,          "limit result set to N items", 0);
+    psMetadataAddBool(exportrunArgs, PS_LIST_TAIL, "-clean",  0,          "mark tables as cleaned", false);
+
+    // -importrun
+    psMetadata *importrunArgs = psMetadataAlloc();
+    psMetadataAddStr(importrunArgs, PS_LIST_TAIL, "-infile",  0,          "import from this file (required)", NULL);
+
+    // -addcamera
+    psMetadata *addcameraArgs = psMetadataAlloc();
+    psMetadataAddStr(addcameraArgs, PS_LIST_TAIL, "-xcamera",  0,      "camera which can supply stacks (required)", NULL);
+
+    // -getcamera
+    psMetadata *getcameraArgs = psMetadataAlloc();
+    psMetadataAddStr(getcameraArgs, PS_LIST_TAIL, "-xcamera",  0,      "get camera_id for this camera", NULL);
+
+    // -listcameras
+    psMetadata *listcamerasArgs = psMetadataAlloc();
+    psMetadataAddBool(listcamerasArgs, PS_LIST_TAIL, "-simple",  0,      "use the simple output format", NULL);
+
+    psFree(now);
+
+    psMetadata *argSets = psMetadataAlloc();
+    psMetadata *modes   = psMetadataAlloc();
+
+    PXOPT_ADD_MODE("-definebyquery",         "Define a new xcstackRun by searching for stacks from one camera", XCSTACKTOOL_MODE_DEFINEBYQUERY, definebyqueryArgs);
+    PXOPT_ADD_MODE("-addbyquery",            "Add stacks from another camera to existing stackRuns",            XCSTACKTOOL_MODE_ADDBYQUERY,    addbyqueryArgs);
+    PXOPT_ADD_MODE("-updaterun",             "",                                                                XCSTACKTOOL_MODE_UPDATERUN,             updaterunArgs);
+
+    PXOPT_ADD_MODE("-tosum",                 "",                                                                XCSTACKTOOL_MODE_TOSUM,                 tosumArgs);
+    PXOPT_ADD_MODE("-inputcameras",          "",                                                                XCSTACKTOOL_MODE_INPUTCAMERAS,          inputcamerasArgs);
+    PXOPT_ADD_MODE("-inputskyfile",          "",                                                                XCSTACKTOOL_MODE_INPUTSKYFILE,          inputskyfileArgs);
+
+    PXOPT_ADD_MODE("-addsumskyfile",         "",                                                                XCSTACKTOOL_MODE_ADDSUMSKYFILE,         addsumskyfileArgs);
+    PXOPT_ADD_MODE("-sumskyfile",            "list results of stackRun",                                        XCSTACKTOOL_MODE_SUMSKYFILE,            sumskyfileArgs);
+    PXOPT_ADD_MODE("-revertsumskyfile",      "",                                                                XCSTACKTOOL_MODE_REVERTSUMSKYFILE,      revertsumskyfileArgs);
+    PXOPT_ADD_MODE("-updatesumskyfile",      "",                                                                XCSTACKTOOL_MODE_UPDATESUMSKYFILE,      updatesumskyfileArgs);
+
+    PXOPT_ADD_MODE("-pendingcleanuprun",     "show runs that need to be cleaned up",                            XCSTACKTOOL_MODE_PENDINGCLEANUPRUN,     pendingcleanuprunArgs);
+    PXOPT_ADD_MODE("-pendingcleanupskyfile", "show runs that need to be cleaned up",                            XCSTACKTOOL_MODE_PENDINGCLEANUPSKYFILE, pendingcleanupskyfileArgs);
+    PXOPT_ADD_MODE("-donecleanup",           "show runs that have been cleaned",                                XCSTACKTOOL_MODE_DONECLEANUP,           donecleanupArgs);
+
+    PXOPT_ADD_MODE("-exportrun",             "export run for import on other database",                         XCSTACKTOOL_MODE_EXPORTRUN,             exportrunArgs);
+    PXOPT_ADD_MODE("-importrun",             "import run from metadata file",                                   XCSTACKTOOL_MODE_IMPORTRUN,             importrunArgs);
+
+    PXOPT_ADD_MODE("-addcamera",             "add camera which can supply stacks",                              XCSTACKTOOL_MODE_ADDCAMERA,             addcameraArgs);
+    PXOPT_ADD_MODE("-getcamera",             "add camera which can supply stacks",                              XCSTACKTOOL_MODE_GETCAMERA,             getcameraArgs);
+    PXOPT_ADD_MODE("-listcameras",           "list cameras which can supply stacks",                            XCSTACKTOOL_MODE_LISTCAMERAS,           listcamerasArgs);
+
+    if (!pxGetOptions(stderr, argc, argv, config, modes, argSets)) {
+        psError(PS_ERR_UNKNOWN, true, "option parsing failed");
+        psFree(argSets);
+        psFree(modes);
+        psFree(config);
+        return NULL;
+    }
+
+    psFree(argSets);
+    psFree(modes);
+
+    // define Database handle, if used
+    // do this last so we don't setup a connection before CLI options are
+    // validated
+    config->dbh = psMemIncrRefCounter(pmConfigDB(config->modules));
+    if (!config->dbh) {
+        psError(PS_ERR_UNKNOWN, false, "Can't configure database");
+        psFree(config);
+        return NULL;
+    }
+
+    return config;
+}
