Index: trunk/ippdb/src/ippdb.c
===================================================================
--- trunk/ippdb/src/ippdb.c	(revision 9247)
+++ trunk/ippdb/src/ippdb.c	(revision 9301)
@@ -57,4 +57,6 @@
 #define P3PENDINGEXP_TABLE_NAME "p3PendingExp"
 #define P3PENDINGEXP_INDEX_NAME IPPDB_DEFAULT_INDEX_NAME
+#define P3PROCESSEDEXP_TABLE_NAME "p3ProcessedExp"
+#define P3PROCESSEDEXP_INDEX_NAME IPPDB_DEFAULT_INDEX_NAME
 #define DETRUN_TABLE_NAME "detRun"
 #define DETRUN_INDEX_NAME IPPDB_DEFAULT_INDEX_NAME
@@ -12858,4 +12860,741 @@
     return true;
 }
+static void p3ProcessedExpRowFree(p3ProcessedExpRow *object);
+
+p3ProcessedExpRow *p3ProcessedExpRowAlloc(const char *exp_tag, const char *uri, const char *recipe, psF64 bg, psF64 bg_stdev, psF64 bg_mean_stdev, psF32 sigma_ra, psF32 sigma_dec, psS32 nastro, const char *b1_uri, const char *b2_uri, psS32 p2_version, psS32 p3_version)
+{
+    p3ProcessedExpRow *_object;
+
+    _object = psAlloc(sizeof(p3ProcessedExpRow));
+    psMemSetDeallocator(_object, (psFreeFunc)p3ProcessedExpRowFree);
+
+    _object->exp_tag = psStringCopy(exp_tag);
+    _object->uri = psStringCopy(uri);
+    _object->recipe = psStringCopy(recipe);
+    _object->bg = bg;
+    _object->bg_stdev = bg_stdev;
+    _object->bg_mean_stdev = bg_mean_stdev;
+    _object->sigma_ra = sigma_ra;
+    _object->sigma_dec = sigma_dec;
+    _object->nastro = nastro;
+    _object->b1_uri = psStringCopy(b1_uri);
+    _object->b2_uri = psStringCopy(b2_uri);
+    _object->p2_version = p2_version;
+    _object->p3_version = p3_version;
+
+    return _object;
+}
+
+static void p3ProcessedExpRowFree(p3ProcessedExpRow *object)
+{
+    psFree(object->exp_tag);
+    psFree(object->uri);
+    psFree(object->recipe);
+    psFree(object->b1_uri);
+    psFree(object->b2_uri);
+}
+
+bool p3ProcessedExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P3PROCESSEDEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P3PROCESSEDEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_tag", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_tag");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg_stdev", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg_stdev");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg_mean_stdev", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg_mean_stdev");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigma_ra", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigma_ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigma_dec", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigma_dec");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nastro", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nastro");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "b1_uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item b1_uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "b2_uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item b2_uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p3_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P3PROCESSEDEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p3ProcessedExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P3PROCESSEDEXP_TABLE_NAME);
+}
+
+bool p3ProcessedExpInsert(psDB * dbh, const char *exp_tag, const char *uri, const char *recipe, psF64 bg, psF64 bg_stdev, psF64 bg_mean_stdev, psF32 sigma_ra, psF32 sigma_dec, psS32 nastro, const char *b1_uri, const char *b2_uri, psS32 p2_version, psS32 p3_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_tag", 0, NULL, exp_tag)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_tag");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg", 0, NULL, bg)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg_stdev", 0, NULL, bg_stdev)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg_stdev");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg_mean_stdev", 0, NULL, bg_mean_stdev)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg_mean_stdev");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigma_ra", 0, NULL, sigma_ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigma_ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigma_dec", 0, NULL, sigma_dec)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigma_dec");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nastro", 0, NULL, nastro)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nastro");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "b1_uri", 0, NULL, b1_uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item b1_uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "b2_uri", 0, NULL, b2_uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item b2_uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, p3_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p3_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P3PROCESSEDEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+long long p3ProcessedExpDelete(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    long long count = psDBDeleteRows(dbh, P3PROCESSEDEXP_TABLE_NAME, where, limit);
+    if (count < 0) {
+        psError(PS_ERR_UNKNOWN, true, "failed to delete row from p3ProcessedExp");
+        return count;
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+bool p3ProcessedExpPop(psDB *dbh, char **exp_tag, char **uri, char **recipe, psF64 *bg, psF64 *bg_stdev, psF64 *bg_mean_stdev, psF32 *sigma_ra, psF32 *sigma_dec, psS32 *nastro, char **b1_uri, char **b2_uri, psS32 *p2_version, psS32 *p3_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P3PROCESSEDEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P3PROCESSEDEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P3PROCESSEDEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P3PROCESSEDEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P3PROCESSEDEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P3PROCESSEDEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_tag = psMetadataLookupPtr(&status, row, "exp_tag");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_tag");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *bg = psMetadataLookupF64(&status, row, "bg");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bg");
+        psFree(row);
+        return false;
+    }
+    *bg_stdev = psMetadataLookupF64(&status, row, "bg_stdev");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bg_stdev");
+        psFree(row);
+        return false;
+    }
+    *bg_mean_stdev = psMetadataLookupF64(&status, row, "bg_mean_stdev");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bg_mean_stdev");
+        psFree(row);
+        return false;
+    }
+    *sigma_ra = psMetadataLookupF32(&status, row, "sigma_ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigma_ra");
+        psFree(row);
+        return false;
+    }
+    *sigma_dec = psMetadataLookupF32(&status, row, "sigma_dec");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigma_dec");
+        psFree(row);
+        return false;
+    }
+    *nastro = psMetadataLookupS32(&status, row, "nastro");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nastro");
+        psFree(row);
+        return false;
+    }
+    *b1_uri = psMetadataLookupPtr(&status, row, "b1_uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item b1_uri");
+        psFree(row);
+        return false;
+    }
+    *b2_uri = psMetadataLookupPtr(&status, row, "b2_uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item b2_uri");
+        psFree(row);
+        return false;
+    }
+    *p2_version = psMetadataLookupS32(&status, row, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        psFree(row);
+        return false;
+    }
+    *p3_version = psMetadataLookupS32(&status, row, "p3_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p3_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p3ProcessedExpInsertObject(psDB *dbh, p3ProcessedExpRow *object)
+{
+    return p3ProcessedExpInsert(dbh, object->exp_tag, object->uri, object->recipe, object->bg, object->bg_stdev, object->bg_mean_stdev, object->sigma_ra, object->sigma_dec, object->nastro, object->b1_uri, object->b2_uri, object->p2_version, object->p3_version);
+}
+
+bool p3ProcessedExpInsertObjects(psDB *dbh, psArray *objects)
+{
+    for (long i = 0; i < psArrayLength(objects); i++) {
+        if (!p3ProcessedExpInsertObject(dbh, objects->data[i])) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+p3ProcessedExpRow *p3ProcessedExpPopObject(psDB *dbh)
+{
+    char            exp_tag[256];
+    char            uri[256];
+    char            recipe[256];
+    psF64           bg;
+    psF64           bg_stdev;
+    psF64           bg_mean_stdev;
+    psF32           sigma_ra;
+    psF32           sigma_dec;
+    psS32           nastro;
+    char            b1_uri[256];
+    char            b2_uri[256];
+    psS32           p2_version;
+    psS32           p3_version;
+
+    if (!p3ProcessedExpPop(dbh, (char **)&exp_tag, (char **)&uri, (char **)&recipe, &bg, &bg_stdev, &bg_mean_stdev, &sigma_ra, &sigma_dec, &nastro, (char **)&b1_uri, (char **)&b2_uri, &p2_version, &p3_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p3ProcessedExpRowAlloc(exp_tag, uri, recipe, bg, bg_stdev, bg_mean_stdev, sigma_ra, sigma_dec, nastro, b1_uri, b2_uri, p2_version, p3_version);
+}
+
+bool p3ProcessedExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P3PROCESSEDEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P3PROCESSEDEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P3PROCESSEDEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P3PROCESSEDEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p3ProcessedExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p3ProcessedExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P3PROCESSEDEXP_TABLE_NAME, P3PROCESSEDEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %d bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p3ProcessedExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P3PROCESSEDEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P3PROCESSEDEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P3PROCESSEDEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P3PROCESSEDEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p3ProcessedExpMetadataFromObject(const p3ProcessedExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_tag", 0, NULL, object->exp_tag)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_tag");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, object->uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, object->recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg", 0, NULL, object->bg)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg_stdev", 0, NULL, object->bg_stdev)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg_stdev");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "bg_mean_stdev", 0, NULL, object->bg_mean_stdev)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bg_mean_stdev");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigma_ra", 0, NULL, object->sigma_ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigma_ra");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigma_dec", 0, NULL, object->sigma_dec)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigma_dec");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nastro", 0, NULL, object->nastro)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nastro");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "b1_uri", 0, NULL, object->b1_uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item b1_uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "b2_uri", 0, NULL, object->b2_uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item b2_uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, object->p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, object->p3_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p3_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p3ProcessedExpRow *p3ProcessedExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_tag;
+    char            *uri;
+    char            *recipe;
+    psF64           bg;
+    psF64           bg_stdev;
+    psF64           bg_mean_stdev;
+    psF32           sigma_ra;
+    psF32           sigma_dec;
+    psS32           nastro;
+    char            *b1_uri;
+    char            *b2_uri;
+    psS32           p2_version;
+    psS32           p3_version;
+
+    exp_tag = psMetadataLookupPtr(&status, md, "exp_tag");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_tag");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    bg = psMetadataLookupF64(&status, md, "bg");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bg");
+        return false;
+    }
+    bg_stdev = psMetadataLookupF64(&status, md, "bg_stdev");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bg_stdev");
+        return false;
+    }
+    bg_mean_stdev = psMetadataLookupF64(&status, md, "bg_mean_stdev");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bg_mean_stdev");
+        return false;
+    }
+    sigma_ra = psMetadataLookupF32(&status, md, "sigma_ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigma_ra");
+        return false;
+    }
+    sigma_dec = psMetadataLookupF32(&status, md, "sigma_dec");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigma_dec");
+        return false;
+    }
+    nastro = psMetadataLookupS32(&status, md, "nastro");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nastro");
+        return false;
+    }
+    b1_uri = psMetadataLookupPtr(&status, md, "b1_uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item b1_uri");
+        return false;
+    }
+    b2_uri = psMetadataLookupPtr(&status, md, "b2_uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item b2_uri");
+        return false;
+    }
+    p2_version = psMetadataLookupS32(&status, md, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        return false;
+    }
+    p3_version = psMetadataLookupS32(&status, md, "p3_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p3_version");
+        return false;
+    }
+
+    return p3ProcessedExpRowAlloc(exp_tag, uri, recipe, bg, bg_stdev, bg_mean_stdev, sigma_ra, sigma_dec, nastro, b1_uri, b2_uri, p2_version, p3_version);
+}
+psArray *p3ProcessedExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P3PROCESSEDEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P3PROCESSEDEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P3PROCESSEDEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        p3ProcessedExpRow *object = p3ProcessedExpObjectFromMetadata(rowSet->data[i]);
+        psArrayAdd(returnSet, 0, object);
+        psFree(object);
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+bool p3ProcessedExpDeleteObject(psDB *dbh, const p3ProcessedExpRow *object)
+{
+    psMetadata *where = p3ProcessedExpMetadataFromObject(object);
+    long long count = psDBDeleteRows(dbh, P3PROCESSEDEXP_TABLE_NAME, where, 0);
+    psFree(where)
+    if (count < 0) {
+        psError(PS_ERR_UNKNOWN, true, "failed to delete row from p3ProcessedExp");
+        return false;
+    }
+    if (count > 1) {
+        // XXX should this be a psAbort() instead?  It is possible that
+        // having an object match multiple rows was by design.
+        psError(PS_ERR_UNKNOWN, true, "p3ProcessedExpRow object matched more then one row.  Check your database schema");
+        return false;
+    }
+
+    return true;
+}
+long long p3ProcessedExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p3ProcessedExpRow *object = objects->data[i];
+        psMetadata *where = p3ProcessedExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P3PROCESSEDEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p3ProcessedExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+bool p3ProcessedExpPrintObjects(FILE *stream, psArray *objects, bool mdcf)
+{
+    PS_ASSERT_PTR_NON_NULL(objects, false);
+
+    psMetadata *output = psMetadataAlloc();
+    for (long i = 0; i < psArrayLength(objects); i++) {
+        psMetadata *md = p3ProcessedExpMetadataFromObject(objects->data[i]);
+        if (!psMetadataAddMetadata(
+            output,
+            PS_LIST_TAIL,
+            P3PROCESSEDEXP_TABLE_NAME,
+            PS_META_DUPLICATE_OK,
+            NULL,
+            md
+        )) {
+            psError(PS_ERR_UNKNOWN, false, "failed to add metadata");
+            psFree(md);
+            psFree(output);
+            return false;
+        }
+        psFree(md);
+    }
+
+    if (!ippdbPrintMetadataRaw(stream, output, mdcf)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to print metadata");
+        psFree(output);
+    }
+    psFree(output);
+
+    return true;
+}
 static void detRunRowFree(detRunRow *object);
 
