Index: trunk/dbconfig/changes.txt
===================================================================
--- trunk/dbconfig/changes.txt	(revision 24511)
+++ trunk/dbconfig/changes.txt	(revision 24512)
@@ -1128,2 +1128,36 @@
 
 ALTER TABLE warpRun ADD COLUMN reduction VARCHAR(64) AFTER tess_id;
+
+-- Tables to support publishing of detections to a Science Client
+-- Clients to which we send stuff
+CREATE TABLE publishClient (
+    client_id BIGINT AUTO_INCREMENT, -- unique identifier
+    product VARCHAR(64),             -- product name
+    stage VARCHAR(64) NOT NULL, -- stage of interest (chip, camera, diff, etc.)
+    workdir VARCHAR(255) NOT NULL, -- working directory
+    comment VARCHAR(255),            -- for human memory
+    PRIMARY KEY(client_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+-- Publishing a set of data (e.g., a specific diffRun)
+CREATE TABLE publishRun (
+    pub_id BIGINT AUTO_INCREMENT, -- unique identifier
+    client_id BIGINT NOT NULL,  -- link to publishClient
+    stage_id BIGINT NOT NULL,   -- link to various stage tables
+    label VARCHAR(64),          -- label for run
+    state VARCHAR(64),          -- state of run (new, full, etc.)
+    PRIMARY KEY(pub_id),
+    KEY(client_id),
+    KEY(stage_id),
+    KEY(label),
+    KEY(state),
+    FOREIGN KEY(client_id) REFERENCES publishClient(client_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+-- Publishing a file within a set
+CREATE TABLE publishDone (
+    pub_id BIGINT AUTO_INCREMENT, -- link to publishRun
+    path_base VARCHAR(255),     -- base path of output
+    fault SMALLINT NOT NULL DEFAULT 0, -- Fault code
+    PRIMARY KEY(pub_id),
+    KEY(fault),
+    FOREIGN KEY(pub_id) REFERENCES publishRun(pub_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
Index: trunk/dbconfig/ipp.m4
===================================================================
--- trunk/dbconfig/ipp.m4	(revision 24511)
+++ trunk/dbconfig/ipp.m4	(revision 24512)
@@ -26,2 +26,3 @@
 include(rc.md)
 include(receive.md)
+include(publish.md)
Index: trunk/dbconfig/publish.md
===================================================================
--- trunk/dbconfig/publish.md	(revision 24512)
+++ trunk/dbconfig/publish.md	(revision 24512)
@@ -0,0 +1,23 @@
+# Tables for publishing data to a science client
+
+publishClient   METADATA 
+    client_id   S64         0       # Primary Key AUTO_INCREMENT
+    product     STR	    64
+    stage	STR	    64
+    workdir     STR	    255
+    comment     STR         255
+END
+
+publishRun	METADATA
+    pub_id      S64         0       # Primary Key AUTO_INCREMENT
+    client_id   S64         0
+    stage_id    S64         0
+    label       STR         64
+    state       STR         64
+END
+
+publishDone	METADATA
+    pub_id      S64         0       # Primary Key
+    path_base	STR	    255
+    fault	S16	    0
+END
Index: trunk/ippScripts/Build.PL
===================================================================
--- trunk/ippScripts/Build.PL	(revision 24511)
+++ trunk/ippScripts/Build.PL	(revision 24512)
@@ -91,4 +91,5 @@
         scripts/receive_setstatus.pl
         scripts/rcserver_checkstatus.pl
+        scripts/publish_file.pl
     )],
     dist_abstract => 'Scripts for running the Pan-STARRS IPP',
Index: trunk/ippScripts/scripts/publish_file.pl
===================================================================
--- trunk/ippScripts/scripts/publish_file.pl	(revision 24512)
+++ trunk/ippScripts/scripts/publish_file.pl	(revision 24512)
@@ -0,0 +1,198 @@
+#!/usr/bin/env perl
+
+use warnings;
+use strict;
+
+## report the program and machine
+use Sys::Hostname;
+my $host = hostname();
+print "Starting script $0 on $host\n\n";
+
+use DateTime;
+my $mjd_start = DateTime->now->mjd;   # MJD of starting script
+
+use vars qw( $VERSION );
+$VERSION = '0.01';
+
+use IPC::Cmd 0.36 qw( can_run run );
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::List qw( parse_md_list );
+use PS::IPP::Config 1.01 qw( :standard );
+use File::Temp qw( tempfile );
+use File::Basename qw( basename );
+use Carp;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+# Look for programs we need
+my $missing_tools;
+my $pubtool = can_run('pubtool') or (warn "Can't find pubtool" and $missing_tools = 1);
+my $camtool = can_run('camtool') or (warn "Can't find camtool" and $missing_tools = 1);
+my $difftool = can_run('difftool') or (warn "Can't find difftool" and $missing_tools = 1);
+my $ppMops = can_run('ppMops') or (warn "Can't find ppMops" and $missing_tools = 1);
+my $dsreg = can_run('dsreg') or (warn "Can't find dsreg" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+# Parse the command-line arguments
+my ( $pub_id, $camera, $stage, $stage_id, $format, $product, $workdir );
+my ( $dbname, $verbose, $no_update, $save_temps, $redirect );
+
+GetOptions(
+    'pub_id=s'          => \$pub_id, # Publish identifier
+    'camera=s'          => \$camera, # Camera name
+    'stage=s'           => \$stage,       # Stage of interest
+    'stage_id=s'        => \$stage_id,    # Stage identifier
+    'product=s'         => \$product,     # Datastore product name
+    'workdir=s'         => \$workdir,     # Working directory
+    'dbname=s'          => \$dbname,    # Database name
+    'verbose'           => \$verbose,   # Print to stdout
+    'no-update'         => \$no_update, # Don't update the database?
+    'save-temps'        => \$save_temps, # Save temporary files?
+    'redirect-output'   => \$redirect,   # Redirect output to log file?
+    ) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --pub_id --camera --stage --stage_id --product --workdir",
+           -exitval => $PS_EXIT_CONFIG_ERROR) unless
+    defined $pub_id and
+    defined $camera and
+    defined $product and
+    defined $stage and
+    defined $stage_id and
+    defined $workdir;
+
+my $outroot = "$workdir/$product.$pub_id"; # Output root name
+
+my $ipprc = PS::IPP::Config->new( $camera ) or
+    &my_die( "Unable to set up", $pub_id, $PS_EXIT_CONFIG_ERROR ); # IPP configuration
+
+$ipprc->outroot_prepare( $outroot );
+my $logDest = "$outroot.log";
+$ipprc->redirect_output($logDest) or &my_die( "Unable to redirect output", $pub_id, $PS_EXIT_SYS_ERROR ) if $redirect;
+
+my $mdcParser = PS::IPP::Metadata::Config->new;
+
+
+# Retrieve file name of interest
+my %files;                      # Input filenames
+my %zp;                         # Zero points
+{
+    my $command;                # Command to run
+
+    if ($stage eq 'diff') {
+        $command =  "difftool -diffskyfile -diff_id $stage_id";
+    } elsif ($stage eq 'camera') {
+        $command =  "camtool -processedexp -cam_id $stage_id";
+    } else {
+        &my_die( "Unrecognised stage: $stage", $pub_id, $PS_EXIT_CONFIG_ERROR );
+    }
+    $command .= " -dbname $dbname" if defined $dbname;
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    &my_die( "Unable to retrieve filename", $pub_id, $PS_EXIT_SYS_ERROR) unless $success;
+
+    my $metadata = $mdcParser->parse(join "", @$stdout_buf) or
+        &my_die("Unable to parse metadata config", $pub_id, $PS_EXIT_PROG_ERROR);
+
+    my $components = parse_md_list($metadata) or
+        &my_die("Unable to parse metadata list", $pub_id, $PS_EXIT_PROG_ERROR);
+
+    foreach my $comp ( @$components ) {
+        my $path_base = $comp->{path_base}; # Base name for file
+        next if defined $comp->{quality} and $comp->{quality} > 0;
+
+        (carp "Bad zpt_obs or exp_time for component" and next) if not defined $comp->{zpt_obs} or not defined $comp->{exp_time};
+        my $zp = $comp->{zpt_obs} + 2.5 * log($comp->{exp_time}) / log(10);
+
+        if ($stage eq 'diff') {
+            my $skycell_id = $comp->{skycell_id};
+            $files{"$skycell_id.pos"} = $ipprc->filename( "PPSUB.OUTPUT.SOURCES", $path_base );
+            $files{"$skycell_id.neg"} = $ipprc->filename( "PPSUB.INVERSE.SOURCES", $path_base ) if
+                defined $comp->{bothways} and $comp->{bothways};
+            $zp{"$skycell_id.pos"} = $zp;
+            $zp{"$skycell_id.neg"} = $zp if defined $comp->{bothways} and $comp->{bothways};
+        } elsif ($stage eq 'camera') {
+            my $cam_id = $comp->{cam_id};
+            $files{$cam_id} = $ipprc->filename( "PSASTRO.OUTPUT", $path_base );
+            $zp{$cam_id} = $zp;
+        }
+    }
+}
+
+# Prepare for data store input
+my ($listFile, $listFileName) = tempfile("/tmp/publish.$pub_id.list.XXXX", UNLINK => !$save_temps );
+
+# Process each file
+foreach my $comp ( keys %files ) {
+    my $file = $ipprc->file_resolve( $files{$comp} ) or
+        &my_die("Unable to resolve file $files{$comp}", $pub_id, $PS_EXIT_SYS_ERROR);
+    my_die("Unable to find file $file", $pub_id, $PS_EXIT_PROG_ERROR) unless $ipprc->file_exists( $file );
+
+    my $zp = $zp{$comp};
+    if ($product eq "IPP-MOPS") {
+        my $out = $ipprc->file_resolve( "$outroot.$comp.fits" );
+        my $command = "$ppMops $file $zp $out";
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        &my_die( "Unable to translate $file", $pub_id, $PS_EXIT_SYS_ERROR) unless $success;
+        &my_die( "Unable to find translated file $out", $pub_id, $PS_EXIT_SYS_ERROR) unless $ipprc->file_exists( $out );
+        $file = $out;
+    }
+
+    # format: filename|filesize|md5sum|filetype|
+    # note: since we omit filesize and md5sum, dsreg will calculate them
+    print $listFile "$file|||$product|$comp|\n";
+}
+
+
+unless ($no_update) {
+    my $command = "$dsreg --add $stage.$stage_id --copy --abspath --product $product --type $product --list $listFileName";
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    &my_die( "Unable to register with data store", $pub_id, $PS_EXIT_SYS_ERROR) unless $success;
+}
+
+unless ($no_update) {
+    my $command = "$pubtool -add -pub_id $pub_id -path_base $outroot";
+    $command .= " -dbname $dbname" if defined $dbname;
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    &my_die( "Unable to register with data store", $pub_id, $PS_EXIT_SYS_ERROR) unless $success;
+}
+
+
+
+### Pau.
+
+
+sub my_die
+{
+    my $msg = shift;            # Exit message
+    my $pub_id = shift;         # Publish run identifier
+    my $fault = shift;          # Fault code
+
+    $fault = $PS_EXIT_PROG_ERROR unless defined $fault;
+
+    carp($msg);
+    if (defined $pub_id and not $no_update) {
+        my $command = "$pubtool -add";
+        $command .= " -pub_id $pub_id";
+        $command .= " -path_base $outroot";
+        $command .= " -fault $fault";
+        $command .= " -dbname $dbname" if defined $dbname;
+
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+    }
+    exit $fault;
+}
+
+
+__END__
Index: trunk/ippTasks/Makefile.am
===================================================================
--- trunk/ippTasks/Makefile.am	(revision 24511)
+++ trunk/ippTasks/Makefile.am	(revision 24512)
@@ -23,5 +23,6 @@
 	rcserver.pro \
 	pstamp.pro \
-	receive.pro
+	receive.pro \
+	publish.pro
 
 other_files = \
Index: trunk/ippTasks/publish.pro
===================================================================
--- trunk/ippTasks/publish.pro	(revision 24512)
+++ trunk/ippTasks/publish.pro	(revision 24512)
@@ -0,0 +1,198 @@
+## publish.pro: -*- sh -*-
+
+# test for required global variables
+check.globals
+
+book init publishRun
+
+macro publish.status
+  book listbook publishRun
+end
+
+macro publish.reset
+  book init publishRun
+end
+
+macro publish.on
+  task publish.trigger
+    active true
+  end
+  task publish.load
+    active true
+  end
+  task publish.run
+    active true
+  end
+end
+
+macro publish.off
+  task publish.trigger
+    active false
+  end
+  task publish.load
+    active false
+  end
+  task publish.run
+    active false
+  end
+end
+
+
+# this variable will cycle through the known database names
+$publish_trigger_DB = 0
+$publish_load_DB = 0
+
+task	       publish.trigger
+  host         local
+
+  periods      -poll $LOADPOLL
+  periods      -exec $LOADEXEC
+  periods      -timeout 30
+  npending     1
+
+  stdout NULL
+  stderr $LOGDIR/publish.trigger.log
+
+  task.exec
+    $run = pubtool -definerun
+    if ($DB:n == 0)
+      option DEFAULT
+    else
+      # save the DB name for the exit tasks
+      option $DB:$publish_trigger_DB
+      $run = $run -dbname $DB:$publish_trigger_DB
+      $publish_trigger_DB ++
+      if ($publish_trigger_DB >= $DB:n) set publish_trigger_DB = 0
+    end
+    add_poll_labels run
+    command $run
+  end
+
+  # success
+  task.exit    0
+  end
+
+  # locked list
+  task.exit    default
+    showcommand failure
+  end
+
+  task.exit    crash
+    showcommand crash
+  end
+
+  # operation times out?
+  task.exit    timeout
+    showcommand timeout
+  end
+end
+
+task	       publish.load
+  host         local
+
+  periods      -poll $LOADPOLL
+  periods      -exec $LOADEXEC
+  periods      -timeout 30
+  npending     1
+
+  stdout NULL
+  stderr $LOGDIR/publish.load.log
+
+  task.exec
+    $run = pubtool -pending
+    if ($DB:n == 0)
+      option DEFAULT
+    else
+      # save the DB name for the exit tasks
+      option $DB:$publish_load_DB
+      $run = $run -dbname $DB:$publish_load_DB
+      $publish_load_DB ++
+      if ($publish_load_DB >= $DB:n) set publish_load_DB = 0
+    end
+    add_poll_args run
+    command $run
+  end
+
+  # success
+  task.exit    0
+    # convert 'stdout' to book format
+    ipptool2book stdout publishRun -key pub_id -uniq -setword dbname $options:0 -setword pantaskState INIT
+    if ($VERBOSE > 2)
+      book listbook publishRun
+    end
+
+    # delete existing entries in the appropriate pantaskStates
+    process_cleanup publishRun
+  end
+
+  # locked list
+  task.exit    default
+    showcommand failure
+  end
+
+  task.exit    crash
+    showcommand crash
+  end
+
+  # operation times out?
+  task.exit    timeout
+    showcommand timeout
+  end
+end
+
+task	       publish.run
+  periods      -poll $RUNPOLL
+  periods      -exec $RUNEXEC
+  periods      -timeout 300
+
+  task.exec
+    book npages publishRun -var N
+    if ($N == 0) break
+    if ($NETWORK == 0) break
+    
+    book getpage publishRun 0 -var pageName -key pantaskState INIT
+    if ("$pageName" == "NULL") break
+
+    book setword publishRun $pageName pantaskState RUN
+    book getword publishRun $pageName pub_id -var PUB_ID
+    book getword publishRun $pageName camera -var CAMERA
+    book getword publishRun $pageName workdir -var WORKDIR
+    book getword publishRun $pageName product -var PRODUCT
+    book getword publishRun $pageName stage -var STAGE
+    book getword publishRun $pageName stage_id -var STAGE_ID
+    book getword publishRun $pageName dbname -var DBNAME
+
+    stdout $LOGDIR/publish.run.log
+    stderr $LOGDIR/publish.run.log
+
+    $run = publish_file.pl --pub_id $PUB_ID --camera $CAMERA --workdir $WORKDIR --product $PRODUCT --stage $STAGE --stage_id $STAGE_ID --redirect-output
+    add_standard_args run
+
+    # save the pageName for future reference below
+    options $pageName
+
+    # create the command line
+    if ($VERBOSE > 1)
+      echo command $run
+    end
+    command $run
+  end
+
+  # default exit status
+  task.exit    default
+    process_exit publishRun $options:0 $JOB_STATUS
+  end
+
+  # locked list
+  task.exit    crash
+    showcommand crash
+    echo "hostname: $JOB_HOSTNAME"
+    book setword publishRun $options:0 pantaskState CRASH
+  end
+
+  # operation timed out?
+  task.exit    timeout
+    showcommand timeout
+    book setword publishRun $options:0 pantaskState TIMEOUT
+  end
+end
Index: trunk/ippTools/doc/publish_flow.txt
===================================================================
--- trunk/ippTools/doc/publish_flow.txt	(revision 24512)
+++ trunk/ippTools/doc/publish_flow.txt	(revision 24512)
@@ -0,0 +1,14 @@
+### Setup
+pubtool -defineclient -stage diff -comment Comment -product IPP-MOPS -workdir /path/to/somewhere
+
+### Regularly run by pantasks to generate publishing runs based on data that has been processed
+pubtool -definerun
+
+### Regularly run by pantasks to get list of files to publish
+pubtool -pending
+--> publish_file.pl --pub_id 12345 --workdir /path/to/somewhere --product IPP-MOPS --stage diff --stage_id 67890
+----> pubtool -add -pub_id 12345 -path_base /path/to/somewherenice -fault 0
+
+### Run as required
+pubtool -revert
+
Index: trunk/ippTools/share/Makefile.am
===================================================================
--- trunk/ippTools/share/Makefile.am	(revision 24511)
+++ trunk/ippTools/share/Makefile.am	(revision 24512)
@@ -168,4 +168,7 @@
      pxadmin_create_mirror_tables.sql \
      pxadmin_drop_tables.sql \
+     pubtool_definerun.sql \
+     pubtool_pending.sql \
+     pubtool_revert.sql \
      pztool_find_completed_exp.sql \
      pztool_pendingimfile.sql \
Index: trunk/ippTools/share/camtool_find_processedexp.sql
===================================================================
--- trunk/ippTools/share/camtool_find_processedexp.sql	(revision 24511)
+++ trunk/ippTools/share/camtool_find_processedexp.sql	(revision 24512)
@@ -5,4 +5,5 @@
     rawExp.exp_tag,
     rawExp.exp_name,
+    rawExp.exp_time,
     rawExp.camera,
     rawExp.telescope,
Index: trunk/ippTools/share/difftool_skyfile.sql
===================================================================
--- trunk/ippTools/share/difftool_skyfile.sql	(revision 24511)
+++ trunk/ippTools/share/difftool_skyfile.sql	(revision 24512)
@@ -5,4 +5,11 @@
     diffRun.state,
     diffRun.workdir,
+    diffRun.bothways,
+    camProcessedExp.zpt_obs,
+    camProcessedExp.zpt_stdev,
+    camProcessedExp.zpt_lq,
+    camProcessedExp.zpt_uq,
+    rawExp.exp_time,
+    rawExp.camera,
     warp1,
     stack1,
@@ -13,7 +20,11 @@
 JOIN diffInputSkyfile USING(diff_id, skycell_id)
 JOIN warpRun
+-- NOTE: joining input only!
+-- This is so that we can get the correct zero point
+-- XXX This needs to be more clever to handle diffs between stacks
     ON warpRun.warp_id = diffInputSkyfile.warp1
 JOIN fakeRun USING(fake_id)
 JOIN camRun USING(cam_id)
+JOIN camProcessedExp USING(cam_id)
 JOIN chipRun USING(chip_id)
 JOIN rawExp USING(exp_id)
Index: trunk/ippTools/share/pubtool_definerun.sql
===================================================================
--- trunk/ippTools/share/pubtool_definerun.sql	(revision 24512)
+++ trunk/ippTools/share/pubtool_definerun.sql	(revision 24512)
@@ -0,0 +1,28 @@
+-- Get runs to publish
+SELECT
+    client_id,
+    stage_id
+FROM (
+    -- Get diffs to publish
+    SELECT
+        client_id,
+        diff_id AS stage_id
+    FROM publishClient
+    JOIN diffRun
+    WHERE publishClient.stage = 'diff'
+        AND diffRun.state = 'full'
+    -- WHERE hook %s
+    UNION
+    -- Get cameras to publish
+    SELECT
+        client_id,
+        cam_id AS stage_id
+    FROM publishClient
+    JOIN camRun
+    WHERE publishClient.stage = 'camera'
+        AND camRun.state = 'full'
+    -- WHERE hook %s
+    ) AS publishToDo
+-- Only get stuff that hasn't been published
+LEFT JOIN publishRun USING(client_id, stage_id)
+WHERE publishRun.client_id IS NULL
Index: trunk/ippTools/share/pubtool_pending.sql
===================================================================
--- trunk/ippTools/share/pubtool_pending.sql	(revision 24512)
+++ trunk/ippTools/share/pubtool_pending.sql	(revision 24512)
@@ -0,0 +1,48 @@
+SELECT
+    publishToDo.*
+FROM (
+    -- Get the diffs
+    -- The following is only appropriate for a diff where warp1 is set; otherwise it's more difficult to get the camera name
+    SELECT DISTINCT
+        publishRun.pub_id,
+        publishClient.product,
+        publishClient.stage,
+        publishClient.workdir,
+        diffRun.diff_id AS stage_id,
+        rawExp.camera
+    FROM publishRun
+    JOIN publishClient USING(client_id)
+    JOIN diffRun
+        ON diffRun.diff_id = publishRun.stage_id
+    JOIN diffInputSkyfile USING(diff_id)
+    -- Need to do something fancy here to get the camera name for a stack
+    LEFT JOIN warpRun ON warpRun.warp_id = diffInputSkyfile.warp1
+    JOIN fakeRun USING(fake_id)
+    JOIN camRun USING(cam_id)
+    JOIN chipRun USING(chip_id)
+    JOIN rawExp USING(exp_id)
+    WHERE publishClient.stage = 'diff'
+        AND publishRun.state = 'new'
+        AND diffRun.state = 'full'
+        -- WHERE hook %s
+    UNION
+    SELECT
+        publishRun.pub_id,
+        publishClient.product,
+        publishClient.stage,
+        publishClient.workdir,
+        camRun.cam_id AS stage_id,
+        rawExp.camera
+    FROM publishRun
+    JOIN publishClient USING(client_id)
+    JOIN camRun
+        ON camRun.cam_id = publishRun.stage_id
+    JOIN chipRun USING(chip_id)
+    JOIN rawExp USING(exp_id)
+    WHERE publishClient.stage = 'camera'
+        AND publishRun.state ='new'
+        AND camRun.state = 'full'
+        -- WHERE hook %s
+) AS publishToDo
+LEFT JOIN publishDone USING(pub_id)
+WHERE publishDone.pub_id IS NULL
Index: trunk/ippTools/share/pubtool_revert.sql
===================================================================
--- trunk/ippTools/share/pubtool_revert.sql	(revision 24512)
+++ trunk/ippTools/share/pubtool_revert.sql	(revision 24512)
@@ -0,0 +1,5 @@
+DELETE FROM publishDone
+USING publishDone, publishRun, publishClient
+WHERE publishDone.pub_id = publishRun.pub_id
+    AND publishRun.client_id = publishClient.client_id
+    AND publishDone.fault != 0
Index: trunk/ippTools/share/pxadmin_create_tables.sql
===================================================================
--- trunk/ippTools/share/pxadmin_create_tables.sql	(revision 24511)
+++ trunk/ippTools/share/pxadmin_create_tables.sql	(revision 24512)
@@ -1369,4 +1369,43 @@
 
 
+
+-- Tables to support publishing of detections to a Science Client
+
+-- Clients to which we send stuff
+CREATE TABLE publishClient (
+    client_id BIGINT AUTO_INCREMENT, -- unique identifier
+    product VARCHAR(64),             -- product name
+    stage VARCHAR(64) NOT NULL, -- stage of interest (chip, camera, diff, etc.)
+    workdir VARCHAR(255) NOT NULL, -- working directory
+    comment VARCHAR(255),            -- for human memory
+    PRIMARY KEY(client_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+-- Publishing a set of data (e.g., a specific diffRun)
+CREATE TABLE publishRun (
+    pub_id BIGINT AUTO_INCREMENT, -- unique identifier
+    client_id BIGINT NOT NULL,  -- link to publishClient
+    stage_id BIGINT NOT NULL,   -- link to various stage tables
+    label VARCHAR(64),          -- label for run
+    state VARCHAR(64),          -- state of run (new, full, etc.)
+    PRIMARY KEY(pub_id),
+    KEY(client_id),
+    KEY(stage_id),
+    KEY(label),
+    KEY(state),
+    FOREIGN KEY(client_id) REFERENCES publishClient(client_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+-- Publishing a file within a set
+CREATE TABLE publishDone (
+    pub_id BIGINT AUTO_INCREMENT, -- link to publishRun
+    path_base VARCHAR(255),     -- base path of output
+    fault SMALLINT NOT NULL DEFAULT 0, -- Fault code
+    PRIMARY KEY(pub_id),
+    KEY(fault),
+    FOREIGN KEY(pub_id) REFERENCES publishRun(pub_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+
 -- This comment line is here to avoid empty query error.
 -- Another way to avoid that problem is to omit the semicolon above but I think that is untidy.
Index: trunk/ippTools/src/Makefile.am
===================================================================
--- trunk/ippTools/src/Makefile.am	(revision 24511)
+++ trunk/ippTools/src/Makefile.am	(revision 24512)
@@ -20,5 +20,6 @@
 	stacktool \
 	warptool \
-	receivetool
+	receivetool \
+	pubtool
 
 
@@ -59,5 +60,6 @@
 	regtool.h \
 	stacktool.h \
-	warptool.h
+	warptool.h \
+	pubtool.h
 
 lib_LTLIBRARIES = libpxtools.la
@@ -220,4 +222,10 @@
     receivetoolConfig.c
 
+pubtool_CFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(IPPDB_CFLAGS)
+pubtool_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(IPPDB_LIBS) libpxtools.la
+pubtool_SOURCES = \
+    pubtool.c \
+    pubtoolConfig.c
+
 clean-local:
 	-rm -f TAGS
Index: trunk/ippTools/src/pubtool.c
===================================================================
--- trunk/ippTools/src/pubtool.c	(revision 24512)
+++ trunk/ippTools/src/pubtool.c	(revision 24512)
@@ -0,0 +1,336 @@
+/*
+ * pubtool.c
+ *
+ * Copyright (C) 2008
+ *
+ * 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 <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <inttypes.h>
+
+#include "pxtools.h"
+#include "pxdata.h"
+#include "pubtool.h"
+
+static bool defineclientMode(pxConfig *config);
+static bool definerunMode(pxConfig *config);
+static bool pendingMode(pxConfig *config);
+static bool addMode(pxConfig *config);
+static bool revertMode(pxConfig *config);
+
+# define MODECASE(caseName, func) \
+    case caseName: \
+    if (!func(config)) { \
+        goto FAIL; \
+    } \
+    break;
+
+
+int main(int argc, char **argv)
+{
+    psLibInit(NULL);
+
+    pxConfig *config = pubtoolConfig(NULL, argc, argv);
+    if (!config) {
+        psError(PXTOOLS_ERR_CONFIG, false, "failed to configure");
+        goto FAIL;
+    }
+
+    switch (config->mode) {
+        MODECASE(PUBTOOL_MODE_DEFINECLIENT, defineclientMode);
+        MODECASE(PUBTOOL_MODE_DEFINERUN, definerunMode);
+        MODECASE(PUBTOOL_MODE_PENDING, pendingMode);
+        MODECASE(PUBTOOL_MODE_ADD, addMode);
+        MODECASE(PUBTOOL_MODE_REVERT, revertMode);
+      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 defineclientMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required
+    PXOPT_LOOKUP_STR(product, config->args, "-product",  true, false);
+    PXOPT_LOOKUP_STR(stage, config->args, "-stage", true, false);
+    PXOPT_LOOKUP_STR(workdir, config->args, "-workdir",  true, false);
+
+    // optional
+    PXOPT_LOOKUP_STR(comment, config->args, "-comment",  false, false);
+
+    if (!publishClientInsert(config->dbh, 0, product, stage, workdir, comment)) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        return false;
+    }
+
+    return true;
+}
+
+static bool definerunMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc(); // WHERE conditions
+
+    // required
+
+    // optional
+    PXOPT_COPY_S64(config->args, where, "-client_id", "client_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-label", "label", "==");
+
+    PXOPT_LOOKUP_STR(label, config->args, "-label", false, false);
+
+    psString query = pxDataGet("pubtool_definerun.sql"); // Query to run
+    if (!query) {
+        psError(PXTOOLS_ERR_DATA, false, "Failed to retreive SQL statement");
+        psFree(where);
+        return false;
+    }
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        psFree(where);
+        return false;
+    }
+
+    psString whereClause = psStringCopy(""); // Additional constraints to add to query
+    if (psListLength(where->list)) {
+        psString clause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&whereClause, "\n AND %s", clause);
+        psFree(clause);
+    }
+    psFree(where);
+
+    if (!p_psDBRunQueryF(config->dbh, query, whereClause, whereClause)) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        psFree(query);
+        psFree(whereClause);
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "Database error");
+        }
+        return false;
+    }
+    psFree(query);
+    psFree(whereClause);
+
+    psArray *output = p_psDBFetchResult(config->dbh); // Output of SELECT statement
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "Database error");
+        }
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("pubtool", PS_LOG_INFO, "No rows found");
+        psFree(output);
+        return true;
+    }
+
+    for (int i = 0; i < output->n; i++) {
+        psMetadata *row = output->data[i]; // Row of interest
+        psS64 client = psMetadataLookupS64(NULL, row, "client_id"); // Client identifier
+        psS64 stage = psMetadataLookupS64(NULL, row, "stage_id");   // Stage identifier
+
+        if (!publishRunInsert(config->dbh, 0, client, stage, label, "new")) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to add fileset");
+            psFree(output);
+            if (!psDBRollback(config->dbh)) {
+                psError(PS_ERR_UNKNOWN, false, "Database error");
+            }
+            return false;
+        }
+    }
+    psFree(output);
+
+    if (!psDBCommit(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        return false;
+    }
+
+    return true;
+}
+
+static bool pendingMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc(); // WHERE conditions
+
+    // required
+    PXOPT_COPY_STR(config->args, where, "-client_id", "publishClient.client_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-stage", "publishClient.stage", "==");
+    PXOPT_COPY_STR(config->args, where, "-comment", "publishClient.comment", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-label", "publishRun.label", "==");
+
+    // optional
+    PXOPT_LOOKUP_U64(limit, config->args, "-limit", false, false);
+    PXOPT_LOOKUP_BOOL(simple, config->args, "-simple", false);
+
+    psString query = pxDataGet("pubtool_pending.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_DATA, false, "Failed to retreive SQL statement");
+        psFree(where);
+        return false;
+    }
+
+    psString whereClause = psStringCopy(""); // WHERE conditions to add
+    if (psListLength(where->list)) {
+        psString clause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&whereClause, "\nAND %s", clause);
+        psFree(clause);
+    }
+    psFree(where);
+
+    if (limit) {
+        psString limitString = psDBGenerateLimitSQL(limit);
+        psStringAppend(&query, " %s", limitString);
+        psFree(limitString);
+    }
+
+    if (!p_psDBRunQueryF(config->dbh, query, whereClause, 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) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        return false;
+    }
+    if (!psArrayLength(output)) {
+        psTrace("pubtool", PS_LOG_INFO, "No rows found");
+        psFree(output);
+        return true;
+    }
+    if (!ippdbPrintMetadatas(stdout, output, "publishRun", !simple)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to print array");
+        psFree(output);
+        return false;
+    }
+    psFree(output);
+
+    return true;
+}
+
+static bool addMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // required
+    PXOPT_LOOKUP_S64(pub_id, config->args, "-pub_id", true, false);
+    PXOPT_LOOKUP_STR(path_base, config->args, "-path_base",  true, false);
+
+    // optional
+    PXOPT_LOOKUP_S32(fault, config->args, "-fault", false, false);
+
+    if (!psDBTransaction(config->dbh)) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        return false;
+    }
+
+    if (!publishDoneInsert(config->dbh, pub_id, path_base, fault)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add file");
+        if (!psDBRollback(config->dbh)) {
+            psError(PS_ERR_UNKNOWN, false, "Database error");
+        }
+        return false;
+    }
+
+    if (fault == 0) {
+        if (!p_psDBRunQueryF(config->dbh,
+                             "UPDATE publishRun SET state = 'full' WHERE pub_id = %" PRId64,
+                             pub_id)) {
+            psError(PS_ERR_UNKNOWN, false, "Database error");
+            if (!psDBRollback(config->dbh)) {
+            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 revertMode(pxConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *where = psMetadataAlloc(); // WHERE conditions
+    PXOPT_COPY_S64(config->args, where, "-pub_id", "publishRun.pub_id", "==");
+    PXOPT_COPY_S32(config->args, where, "-fault", "publishDone.fault", "==");
+    PXOPT_COPY_STR(config->args, where, "-client_id", "publishClient.client_id", "==");
+    PXOPT_COPY_STR(config->args, where, "-comment", "publishClient.comment", "LIKE");
+    PXOPT_COPY_STR(config->args, where, "-label", "publishRun.label", "==");
+
+    psString query = pxDataGet("pubtool_revert.sql");
+    if (!query) {
+        psError(PXTOOLS_ERR_DATA, false, "Failed to retreive SQL statement");
+        psFree(where);
+        return false;
+    }
+
+    if (psListLength(where->list)) {
+        psString whereClause = psDBGenerateWhereConditionSQL(where, NULL);
+        psStringAppend(&query, " AND %s", whereClause);
+        psFree(whereClause);
+    }
+    psFree(where);
+
+    if (!p_psDBRunQuery(config->dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "Database error");
+        psFree(query);
+        return false;
+    }
+    psFree(query);
+
+    return true;
+}
+
Index: trunk/ippTools/src/pubtool.h
===================================================================
--- trunk/ippTools/src/pubtool.h	(revision 24512)
+++ trunk/ippTools/src/pubtool.h	(revision 24512)
@@ -0,0 +1,36 @@
+/*
+ * pubtool.h
+ *
+ * Copyright (C) 2008 IfA
+ *
+ * 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 PUBTOOL_H
+#define PUBTOOL_H 1
+
+#include "pxtools.h"
+
+typedef enum {
+    PUBTOOL_MODE_NONE      = 0x0,
+    PUBTOOL_MODE_DEFINECLIENT,
+    PUBTOOL_MODE_DEFINERUN,
+    PUBTOOL_MODE_PENDING,
+    PUBTOOL_MODE_ADD,
+    PUBTOOL_MODE_REVERT,
+} pubtoolMode;
+
+pxConfig *pubtoolConfig(pxConfig *config, int argc, char **argv);
+
+#endif // PUBTOOL_H
Index: trunk/ippTools/src/pubtoolConfig.c
===================================================================
--- trunk/ippTools/src/pubtoolConfig.c	(revision 24512)
+++ trunk/ippTools/src/pubtoolConfig.c	(revision 24512)
@@ -0,0 +1,111 @@
+/*
+ * pubtoolConfig.c
+ *
+ * Copyright (C) 2008
+ *
+ * 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 "pubtool.h"
+
+pxConfig *pubtoolConfig(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(PS_ERR_UNKNOWN, false, "Can't find site configuration!");
+        psFree(config);
+        return NULL;
+    }
+
+    // -defineclient
+    psMetadata *defineclientArgs = psMetadataAlloc();
+    psMetadataAddStr(defineclientArgs, PS_LIST_TAIL, "-stage", 0, "define stage (required)", NULL);
+    psMetadataAddStr(defineclientArgs, PS_LIST_TAIL, "-product", 0, "define product (required)", NULL);
+    psMetadataAddStr(defineclientArgs, PS_LIST_TAIL, "-workdir", 0, "define workdir (required)", NULL);
+    psMetadataAddStr(defineclientArgs, PS_LIST_TAIL, "-comment", 0, "define comment", NULL);
+
+    // -definerun
+    psMetadata *definerunArgs = psMetadataAlloc();
+    psMetadataAddS64(definerunArgs, PS_LIST_TAIL, "-client_id", 0, "search by client_id", 0);
+    psMetadataAddStr(definerunArgs, PS_LIST_TAIL, "-label", 0, "set and search by label", NULL);
+
+    // -pending
+    psMetadata *pendingArgs = psMetadataAlloc();
+    psMetadataAddStr(pendingArgs, PS_LIST_TAIL, "-client_id", 0, "search on client_id", NULL);
+    psMetadataAddStr(pendingArgs, PS_LIST_TAIL, "-stage", 0, "search on source", NULL);
+    psMetadataAddStr(pendingArgs, PS_LIST_TAIL, "-comment", 0, "search on comment (LIKE)", NULL);
+    psMetadataAddStr(pendingArgs, PS_LIST_TAIL, "-label", 0, "search on label", NULL);
+    psMetadataAddBool(pendingArgs, PS_LIST_TAIL, "-simple",  0, "use simple output format?", false);
+    psMetadataAddU64(pendingArgs, PS_LIST_TAIL, "-limit",  0, "limit result set", 0);
+
+    // -add
+    psMetadata *addArgs = psMetadataAlloc();
+    psMetadataAddS64(addArgs, PS_LIST_TAIL, "-pub_id", 0, "define pub_id (required)", 0);
+    psMetadataAddStr(addArgs, PS_LIST_TAIL, "-path_base", 0, "define path_base (required)", NULL);
+    psMetadataAddS32(addArgs, PS_LIST_TAIL, "-fault", 0, "define fault code", 0);
+
+    // -revert
+    psMetadata *revertArgs = psMetadataAlloc();
+    psMetadataAddS64(revertArgs, PS_LIST_TAIL, "-pub_id", 0, "search on pub_id", 0);
+    psMetadataAddS32(revertArgs, PS_LIST_TAIL, "-fault", 0, "search on fault code", 0);
+    psMetadataAddStr(revertArgs, PS_LIST_TAIL, "-client_id", 0, "search on client_id", NULL);
+    psMetadataAddStr(revertArgs, PS_LIST_TAIL, "-comment", 0, "search on comment (LIKE)", NULL);
+    psMetadataAddStr(revertArgs, PS_LIST_TAIL, "-label", 0, "search on label", NULL);
+
+
+    psMetadata *argSets = psMetadataAlloc();
+    psMetadata *modes = psMetadataAlloc();
+
+    PXOPT_ADD_MODE("-defineclient", "", PUBTOOL_MODE_DEFINECLIENT, defineclientArgs);
+    PXOPT_ADD_MODE("-definerun", "", PUBTOOL_MODE_DEFINERUN, definerunArgs);
+    PXOPT_ADD_MODE("-pending", "", PUBTOOL_MODE_PENDING, pendingArgs);
+    PXOPT_ADD_MODE("-add", "", PUBTOOL_MODE_ADD, addArgs);
+    PXOPT_ADD_MODE("-revert", "", PUBTOOL_MODE_REVERT, revertArgs);
+
+    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/psLib/src/fits/psFitsTable.c
===================================================================
--- trunk/psLib/src/fits/psFitsTable.c	(revision 24511)
+++ trunk/psLib/src/fits/psFitsTable.c	(revision 24512)
@@ -390,9 +390,11 @@
 
 
-bool psFitsInsertTable(psFits* fits,
-                       const psMetadata* header,
-                       const psArray* table,
-                       const char *extname,
-                       bool after)
+static bool fitsInsertTable(psFits* fits,             // FITS file
+                            const psMetadata* header, // FITS header to write
+                            const psArray* table,     // Table to write
+                            const char *extname,      // Extension name to give table
+                            bool after,               // Write table after current extension?
+                            bool writeData            // Write data?
+                            )
 {
     PS_ASSERT_FITS_NON_NULL(fits, false);
@@ -403,5 +405,5 @@
 
     long numRows = table->n;
-    if (numRows < 1) {
+    if (writeData && numRows < 1) {
         // no table data, what can I do?
         psError(PS_ERR_BAD_PARAMETER_SIZE, true,
@@ -504,5 +506,5 @@
         fits_create_tbl(fits->fd,
                         BINARY_TBL,
-                        table->n, // number of rows in table
+                        writeData ? numRows : 0, // number of rows in table
                         numColumns, // number of columns in table
                         (char**)columnNames->data, // names of the columns
@@ -524,5 +526,5 @@
         // Insert the table
         fits_insert_btbl(fits->fd,
-                         table->n, // number of rows in table
+                         writeData ? numRows : 0, // number of rows in table
                          numColumns, // number of columns in table
                          (char**)columnNames->data, // names of the columns
@@ -562,76 +564,78 @@
 
     // cfitsio requires that we write the data by columns --- urgh!
-    psMetadataIteratorSet(colSpecsIter, PS_LIST_HEAD);
-    for (long colNum = 1; (colSpecItem = psMetadataGetAndIncrement(colSpecsIter)); colNum++) {
-        // Note: colNum is unit-indexed, because it's for cfitsio
-        colSpec *spec = colSpecItem->data.V; // The specification
-        if (PS_DATA_IS_PRIMITIVE(spec->type)) {
-            size_t dataSize = PSELEMTYPE_SIZEOF(spec->type); // Size (in bytes) of this type
-            psVector *columnData = psVectorAlloc(table->n, spec->type); // The raw row data, to be written
-            psVectorInit(columnData, 0);
-            for (long i = 0; i < table->n; i++) {
-                psMetadata *row = table->data[i]; // The row of interest
-                psMetadataItem *dataItem = psMetadataLookup(row, colSpecItem->name); // The value of interest
-                memcpy(&columnData->data.U8[i * dataSize], &dataItem->data, dataSize);
+    if (writeData) {
+        psMetadataIteratorSet(colSpecsIter, PS_LIST_HEAD);
+        for (long colNum = 1; (colSpecItem = psMetadataGetAndIncrement(colSpecsIter)); colNum++) {
+            // Note: colNum is unit-indexed, because it's for cfitsio
+            colSpec *spec = colSpecItem->data.V; // The specification
+            if (PS_DATA_IS_PRIMITIVE(spec->type)) {
+                size_t dataSize = PSELEMTYPE_SIZEOF(spec->type); // Size (in bytes) of this type
+                psVector *columnData = psVectorAlloc(table->n, spec->type); // The raw row data, to be written
+                psVectorInit(columnData, 0);
+                for (long i = 0; i < table->n; i++) {
+                    psMetadata *row = table->data[i]; // The row of interest
+                    psMetadataItem *dataItem = psMetadataLookup(row, colSpecItem->name); // Value of interest
+                    memcpy(&columnData->data.U8[i * dataSize], &dataItem->data, dataSize);
+                }
+
+                int fitsDataType;           // Data type for cfitsio
+                p_psFitsTypeToCfitsio(spec->type, NULL, NULL, &fitsDataType);
+                fits_write_col(fits->fd,
+                               fitsDataType,
+                               colNum, // column number
+                               1, // first row
+                               1, // first element
+                               table->n, // number of rows
+                               columnData->data.U8, // the data
+                               &status);
+                psFree(columnData);
+            } else {
+                switch (spec->type) {
+                  case PS_DATA_STRING: {
+                      psArray *strings = psArrayAlloc(table->n); // Array of strings
+                      for (long i = 0; i < table->n; i++) {
+                          psMetadata *row = table->data[i]; // The row of interest
+                          strings->data[i] = psMemIncrRefCounter(psMetadataLookupStr(NULL, row,
+                                                                                     colSpecItem->name));
+                      }
+                      fits_write_col_str(fits->fd, colNum, 1, 1, table->n, (char**)strings->data, &status);
+                      psFree(strings);
+                      break;
+                  }
+                  case PS_DATA_VECTOR: {
+                      size_t dataSize = PSELEMTYPE_SIZEOF(spec->vectorType); // Size of data, in bytes
+                      psVector *columnData = psVectorAlloc(spec->size * table->n * dataSize, PS_TYPE_U8);
+                      psVectorInit(columnData, 0);
+                      for (long i = 0; i < table->n; i++) {
+                          psMetadata *row = table->data[i]; // The row of interest
+                          psMetadataItem* dataItem = psMetadataLookup(row, colSpecItem->name);
+                          if (dataItem->type != PS_DATA_VECTOR) {
+                              // Just in case --- get a zero instead of some weird result
+                              continue;
+                          }
+                          psVector *vector = dataItem->data.V;
+                          memcpy(&columnData->data.U8[i * dataSize * spec->size], vector->data.U8,
+                                 vector->n * dataSize);
+                      }
+
+                      int fitsDataType;           // Data type for cfitsio
+                      p_psFitsTypeToCfitsio(spec->vectorType, NULL, NULL, &fitsDataType);
+                      fits_write_col(fits->fd, fitsDataType, colNum, 1, 1, table->n * spec->size,
+                                     columnData->data.U8, &status);
+                      psFree(columnData);
+                      break;
+                  }
+                  default:
+                    psAbort("Should never get here.\n");
+                }
             }
 
-            int fitsDataType;           // Data type for cfitsio
-            p_psFitsTypeToCfitsio(spec->type, NULL, NULL, &fitsDataType);
-            fits_write_col(fits->fd,
-                           fitsDataType,
-                           colNum, // column number
-                           1, // first row
-                           1, // first element
-                           table->n, // number of rows
-                           columnData->data.U8, // the data
-                           &status);
-            psFree(columnData);
-        } else {
-            switch (spec->type) {
-            case PS_DATA_STRING: {
-                    psArray *strings = psArrayAlloc(table->n); // Array of strings
-                    for (long i = 0; i < table->n; i++) {
-                        psMetadata *row = table->data[i]; // The row of interest
-                        strings->data[i] = psMemIncrRefCounter(psMetadataLookupStr(NULL, row,
-                                                               colSpecItem->name));
-                    }
-                    fits_write_col_str(fits->fd, colNum, 1, 1, table->n, (char**)strings->data, &status);
-                    psFree(strings);
-                    break;
-                }
-            case PS_DATA_VECTOR: {
-                    size_t dataSize = PSELEMTYPE_SIZEOF(spec->vectorType); // Size of data, in bytes
-                    psVector *columnData = psVectorAlloc(spec->size * table->n * dataSize, PS_TYPE_U8);
-                    psVectorInit(columnData, 0);
-                    for (long i = 0; i < table->n; i++) {
-                        psMetadata *row = table->data[i]; // The row of interest
-                        psMetadataItem* dataItem = psMetadataLookup(row, colSpecItem->name);
-                        if (dataItem->type != PS_DATA_VECTOR) {
-                            // Just in case --- get a zero instead of some weird result
-                            continue;
-                        }
-                        psVector *vector = dataItem->data.V;
-                        memcpy(&columnData->data.U8[i * dataSize * spec->size], vector->data.U8,
-                               vector->n * dataSize);
-                    }
-
-                    int fitsDataType;           // Data type for cfitsio
-                    p_psFitsTypeToCfitsio(spec->vectorType, NULL, NULL, &fitsDataType);
-                    fits_write_col(fits->fd, fitsDataType, colNum, 1, 1, table->n * spec->size,
-                                   columnData->data.U8, &status);
-                    psFree(columnData);
-                    break;
-                }
-            default:
-                psAbort("Should never get here.\n");
+            // Check error status from writing column
+            if (status != 0) {
+                psFitsError(status, true, "Unable to write column %ld of FITS table", colNum);
+                psFree(colSpecsIter);
+                psFree(colSpecs);
+                return false;
             }
-        }
-
-        // Check error status from writing column
-        if (status != 0) {
-            psFitsError(status, true, "Unable to write column %ld of FITS table", colNum);
-            psFree(colSpecsIter);
-            psFree(colSpecs);
-            return false;
         }
     }
@@ -650,4 +654,42 @@
     return true;
 }
+
+
+bool psFitsInsertTable(psFits* fits, const psMetadata* header, const psArray* table, const char *extname,
+                       bool after)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+    return fitsInsertTable(fits, header, table, extname, after, true);
+}
+
+bool psFitsWriteTableEmpty(psFits *fits, const psMetadata *header, const psMetadata *columns,
+                           const char *extname)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+    if (!psFitsMoveLast(fits)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to move to last extension to write table");
+        return false;
+    }
+    psArray *table = psArrayAlloc(1);   // Dummy table carrying column definitions
+    table->data[0] = psMemIncrRefCounter((psPtr)columns); // Casting away const
+    bool status = fitsInsertTable(fits, header, table, extname, true, false); // Status of insertion
+    psFree(table);
+    return status;
+}
+
+bool psFitsInsertTableEmpty(psFits *fits, const psMetadata *header, const psMetadata *columns,
+                            const char *extname, bool after)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+    psArray *table = psArrayAlloc(1);   // Dummy table carrying column definitions
+    table->data[0] = psMemIncrRefCounter((psPtr)columns); // Casting away const
+    bool status = fitsInsertTable(fits, header, table, extname, after, false); // Status of insertion
+    psFree(table);
+    return status;
+}
+
 
 bool psFitsUpdateTable(psFits* fits,
Index: trunk/psLib/src/fits/psFitsTable.h
===================================================================
--- trunk/psLib/src/fits/psFitsTable.h	(revision 24511)
+++ trunk/psLib/src/fits/psFitsTable.h	(revision 24512)
@@ -90,4 +90,12 @@
 );
 
+/// Write an empty table
+bool psFitsWriteTableEmpty(
+    psFits *fits,                       ///< FITS file pointer
+    const psMetadata *header,           ///< Header to write
+    const psMetadata *columns,          ///< Column definitions; no data used except name,type
+    const char *extname                 ///< Extension name for table
+    );
+
 /** Inserts a whole FITS table. A new HDU of the type BINTABLE is inserted either
  *  before or after, depending on the AFTER parameter, the current HDU.
@@ -104,4 +112,14 @@
     bool after    ///< TRUE if insert is done after CHDU, otherwise table is inserted before CHDU
 );
+
+/// Insert an empty table
+bool psFitsInsertTableEmpty(
+    psFits *fits,              ///< FITS file pointer
+    const psMetadata *header,  ///< Header to write
+    const psMetadata *columns, ///< Column definitions; no data used except name,type
+    const char *extname,       ///< Extension name for table
+    bool after                 ///< Insert after current HDU?
+    );
+
 
 /** Updates a FITS table.  The current HDU type must be either
Index: trunk/psconfig/tagsets/ipp-2.8.dist
===================================================================
--- trunk/psconfig/tagsets/ipp-2.8.dist	(revision 24511)
+++ trunk/psconfig/tagsets/ipp-2.8.dist	(revision 24512)
@@ -69,4 +69,6 @@
   YYYYY  DataStore              ipp-2-8          -0
 
+  YYYYY  ppMops                 ipp-2-8          -0
+
   YNNNN  extsrc/gpcsw           ipp-2-8          -0
 
