Index: /branches/eam_branches/ipp-20230313/ippTools/share/Makefile.am
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/Makefile.am	(revision 42540)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/Makefile.am	(revision 42541)
@@ -554,4 +554,15 @@
 	fpcamtool_queue_cam_id.sql \
 	fpcamtool_revertprocessedexp.sql \
-	fpcamtool_revertupdatedexp.sql
+	fpcamtool_revertupdatedexp.sql \
+	xcstacktool_definebyquery_select.sql \
+	xcstacktool_addbyquery_select.sql \
+	xcstacktool_addcamera.sql \
+	xcstacktool_inputskyfile.sql \
+	xcstacktool_tosum.sql \
+	xcstacktool_sumskyfile.sql \
+	xcstacktool_revertsumskyfile_delete.sql \
+	xcstacktool_pendingcleanuprun.sql \
+	xcstacktool_pendingcleanupskyfile.sql \
+	xcstacktool_donecleanup.sql
 
+
Index: /branches/eam_branches/ipp-20230313/ippTools/share/pxadmin_create_tables.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/pxadmin_create_tables.sql	(revision 42540)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/pxadmin_create_tables.sql	(revision 42541)
@@ -2481,4 +2481,62 @@
 ) ENGINE=innodb DEFAULT CHARSET=latin1;
 
+CREATE TABLE xcstackRun (
+        xcstack_id BIGINT AUTO_INCREMENT,
+        state VARCHAR(64),
+        workdir VARCHAR(255),
+        label VARCHAR(64),
+        data_group VARCHAR(64),
+        dist_group VARCHAR(64),
+        reduction VARCHAR(64),
+        registered DATETIME,
+        skycell_id VARCHAR(64),
+        tess_id VARCHAR(64),
+        filter VARCHAR(64),
+        software_ver VARCHAR(16),
+        note VARCHAR(255),
+        PRIMARY KEY(xcstack_id),
+        KEY(xcstack_id),
+        KEY(state),
+        KEY(skycell_id),
+        KEY(tess_id),
+        KEY(label),
+        KEY(data_group),
+        KEY(dist_group)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+CREATE TABLE xcstackInputSkyfile (
+        xcstack_id BIGINT,
+        camera_id BIGINT,
+        stack_id BIGINT,
+        PRIMARY KEY(xcstack_id, stack_id),
+        FOREIGN KEY(xcstack_id) REFERENCES xcstackRun(xcstack_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+CREATE TABLE xcstackSumSkyfile (
+        xcstack_id BIGINT,
+        path_base VARCHAR(255),
+        bg DOUBLE,
+        bg_stdev DOUBLE,
+        dtime_stack FLOAT,
+        dtime_script FLOAT,
+        hostname VARCHAR(64),
+        good_frac FLOAT,
+        fault SMALLINT,
+        software_ver VARCHAR(16),
+        quality SMALLINT NOT NULL DEFAULT 0,
+        PRIMARY KEY(xcstack_id),
+        KEY(dtime_stack),
+        KEY(good_frac),
+        KEY(fault),
+        KEY(quality),
+        FOREIGN KEY(xcstack_id) REFERENCES xcstackRun(xcstack_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+CREATE TABLE xcstackCamera (
+        camera_id BIGINT,
+        camera VARCHAR(255),
+        PRIMARY KEY(camera_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
 -- These comment lines are here to avoid an empty query error.
 -- Another way to avoid that problem is to omit the semicolon above but I think that is untidy.
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_addbyquery_select.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_addbyquery_select.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_addbyquery_select.sql	(revision 42541)
@@ -0,0 +1,30 @@
+-- This is a query to get a list of skycells with warps to be stacked,
+-- along with the number of warps already in a stack.
+
+SELECT
+    TMP.xcstack_id,
+    TMP.stack_id,
+    TMP.tess_id,
+    TMP.skycell_id,
+    TMP.filter
+  FROM
+  (SELECT
+    xcstackRun.xcstack_id,
+    stackRun.stack_id,
+    stackRun.tess_id,
+    stackRun.skycell_id,
+    stackRun.filter
+    FROM %s.stackRun
+    JOIN %s.stackSumSkyfile USING (stack_id)
+    JOIN xcstackRun
+    ON ((stackRun.tess_id = xcstackRun.tess_id)
+    AND (stackRun.skycell_id = xcstackRun.skycell_id)
+    AND (stackRun.filter   = '%s')
+    AND (xcstackRun.filter = '%s'))
+-- WHERE HOOK:
+   WHERE %s
+  ) AS TMP
+    LEFT JOIN xcstackInputSkyfile
+    ON (TMP.stack_id = xcstackInputSkyfile.stack_id)
+    AND (camera_id = %d)
+    WHERE xcstackInputSkyfile.xcstack_id IS NULL
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_addcamera.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_addcamera.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_addcamera.sql	(revision 42541)
@@ -0,0 +1,3 @@
+
+INSERT INTO xcstackCamera
+ (camera_id, camera) VALUES (0, '@CAMERA@')
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_definebyquery_select.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_definebyquery_select.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_definebyquery_select.sql	(revision 42541)
@@ -0,0 +1,15 @@
+-- This is a query to get a list of skycells with warps to be stacked,
+-- along with the number of warps already in a stack.
+
+SELECT
+    xcstack_id,
+    stackRun.stack_id,
+    tess_id,
+    skycell_id,
+    filter
+    FROM %s.stackRun
+    JOIN %s.stackSumSkyfile USING (stack_id)
+    LEFT JOIN xcstackInputSkyfile
+    ON (stackRun.stack_id = xcstackInputSkyfile.stack_id)
+    AND (xcstackInputSkyfile.camera_id = %d)
+    WHERE xcstack_id IS NULL
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_donecleanup.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_donecleanup.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_donecleanup.sql	(revision 42541)
@@ -0,0 +1,5 @@
+SELECT
+    xcstackRun.*
+FROM xcstackRun
+WHERE
+    xcstackRun.state = 'cleaned'
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_inputskyfile.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_inputskyfile.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_inputskyfile.sql	(revision 42541)
@@ -0,0 +1,7 @@
+SELECT
+  stackSumSkyfile.*
+  FROM xcstackInputSkyfile
+  JOIN %s.stackSumSkyfile
+  ON (xcstackInputSkyfile.stack_id = stackSumSkyfile.stack_id)
+  AND (xcstackInputSkyfile.camera_id = %d) where (xcstack_id = %d)
+    
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_pendingcleanuprun.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_pendingcleanuprun.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_pendingcleanuprun.sql	(revision 42541)
@@ -0,0 +1,9 @@
+-- this query only returns xcstackRun entries with a matching xcstackSumSkyfile
+SELECT
+    xcstackRun.xcstack_id,
+    xcstackRun.state,
+    xcstackSumSkyfile.path_base
+FROM xcstackRun
+JOIN xcstackSumSkyfile USING(xcstack_id)
+WHERE
+    (xcstackRun.state = 'goto_cleaned' OR xcstackRun.state = 'goto_scrubbed' OR xcstackRun.state = 'goto_purged')
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_pendingcleanupskyfile.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_pendingcleanupskyfile.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_pendingcleanupskyfile.sql	(revision 42541)
@@ -0,0 +1,11 @@
+SELECT
+    xcstackSumSkyfile.*,
+    xcstackRun.state,
+    xcstackRun.workdir,
+    xcstackRun.tess_id,
+    xcstackRun.skycell_id
+FROM xcstackRun
+JOIN xcstackSumSkyfile
+    USING(xcstack_id)
+WHERE
+    (xcstackRun.state = 'goto_cleaned' OR xcstackRun.state = 'goto_scrubbed' OR xcstackRun.state = 'goto_purged')
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_revertsumskyfile_delete.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_revertsumskyfile_delete.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_revertsumskyfile_delete.sql	(revision 42541)
@@ -0,0 +1,5 @@
+DELETE FROM xcstackSumSkyfile
+USING xcstackSumSkyfile, xcstackRun
+WHERE xcstackSumSkyfile.xcstack_id = xcstackRun.xcstack_id
+    AND xcstackRun.state = 'new'
+    AND fault != 0
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_sumskyfile.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_sumskyfile.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_sumskyfile.sql	(revision 42541)
@@ -0,0 +1,12 @@
+SELECT
+    xcstackSumSkyfile.*,
+    xcstackRun.state,
+    xcstackRun.tess_id,
+    xcstackRun.skycell_id,
+    xcstackRun.filter,
+    xcstackRun.label,
+    xcstackRun.data_group,
+    xcstackRun.workdir
+FROM xcstackRun
+JOIN xcstackSumSkyfile
+USING(xcstack_id)
Index: /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_tosum.sql
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_tosum.sql	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/share/xcstacktool_tosum.sql	(revision 42541)
@@ -0,0 +1,15 @@
+SELECT
+    xcstackRun.xcstack_id,
+    xcstackRun.tess_id,
+    xcstackRun.skycell_id,
+    xcstackRun.workdir,
+    xcstackRun.reduction,
+    xcstackRun.label,
+    xcstackRun.state,
+    IFNULL(Label.priority, 10000) AS priority
+FROM xcstackRun
+LEFT JOIN xcstackSumSkyfile USING(xcstack_id)
+LEFT JOIN Label ON Label.label = xcstackRun.label
+WHERE
+    (xcstackRun.state = 'new' AND xcstackSumSkyfile.xcstack_id IS NULL)
+    AND (Label.active OR Label.active IS NULL)
Index: /branches/eam_branches/ipp-20230313/ippTools/src/Makefile.am
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/src/Makefile.am	(revision 42540)
+++ /branches/eam_branches/ipp-20230313/ippTools/src/Makefile.am	(revision 42541)
@@ -24,4 +24,5 @@
 	stacktool \
 	staticskytool \
+	xcstacktool \
 	warptool \
 	receivetool \
@@ -82,4 +83,5 @@
 	regtool.h \
 	stacktool.h \
+	xcstacktool.h \
 	staticskytool.h \
 	warptool.h \
@@ -245,4 +247,10 @@
     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
+
 pxadmin_CFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(IPPDB_CFLAGS)
 pxadmin_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(IPPDB_LIBS) libpxtools.la
Index: /branches/eam_branches/ipp-20230313/ippTools/src/stacktool.c
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/src/stacktool.c	(revision 42540)
+++ /branches/eam_branches/ipp-20230313/ippTools/src/stacktool.c	(revision 42541)
@@ -496,7 +496,4 @@
             psFree(insert);
             psFree(list);
-            if (!psDBRollback(config->dbh)) {
-                psError(PS_ERR_UNKNOWN, false, "database error");
-            }
             return false;
         }
Index: /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktool.c
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktool.c	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktool.c	(revision 42541)
@@ -0,0 +1,1497 @@
+/*
+ * 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);
+
+    psMetadata *where = psMetadataAlloc();
+
+    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");
+    
+    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_definebyquery_select.sql");
+    if (!select) {
+      psError(PXTOOLS_ERR_SYS, false, "failed to retreive SQL statement");
+      psFree(where);
+      return false;
+    }
+
+    if (psListLength(where->list)) {
+      psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+      psStringAppend(&select, " 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, camera_id)) {
+      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
+					      "new",                      // 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, "xcstackSkycells", !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: /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktool.h
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktool.h	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktool.h	(revision 42541)
@@ -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: /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktoolConfig.c
===================================================================
--- /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktoolConfig.c	(revision 42541)
+++ /branches/eam_branches/ipp-20230313/ippTools/src/xcstacktoolConfig.c	(revision 42541)
@@ -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;
+}
