#include "ippdb.h"

#define WEATHER_TABLE_NAME "weather"
#define WEATHER_INDEX_NAME "position"
#define SKYP_TRANSPARENCY_TABLE_NAME "skyp_transparency"
#define SKYP_TRANSPARENCY_INDEX_NAME "position"
#define SKYP_ABSORPTION_TABLE_NAME "skyp_absorption"
#define SKYP_ABSORPTION_INDEX_NAME "position"
#define SKYP_EMISSION_TABLE_NAME "skyp_emission"
#define SKYP_EMISSION_INDEX_NAME "position"
#define DIMM_TABLE_NAME "dimm"
#define DIMM_INDEX_NAME "position"
#define SKYP_IR_TABLE_NAME "skyp_ir"
#define SKYP_IR_INDEX_NAME "position"
#define DOME_TABLE_NAME "dome"
#define DOME_INDEX_NAME "position"
#define TELESCOPE_TABLE_NAME "telescope"
#define TELESCOPE_INDEX_NAME "position"
#define SUMMITEXP_TABLE_NAME "summitExp"
#define SUMMITEXP_INDEX_NAME "position"
#define PZPENDINGEXP_TABLE_NAME "pzPendingExp"
#define PZPENDINGEXP_INDEX_NAME "position"
#define PZPENDINGIMFILE_TABLE_NAME "pzPendingImfile"
#define PZPENDINGIMFILE_INDEX_NAME "position"
#define NEWEXP_TABLE_NAME "newExp"
#define NEWEXP_INDEX_NAME "position"
#define NEWIMFILE_TABLE_NAME "newImfile"
#define NEWIMFILE_INDEX_NAME "position"
#define RAWDETRENDEXP_TABLE_NAME "rawDetrendExp"
#define RAWDETRENDEXP_INDEX_NAME "position"
#define RAWSCIENCEEXP_TABLE_NAME "rawScienceExp"
#define RAWSCIENCEEXP_INDEX_NAME "position"
#define RAWIMFILE_TABLE_NAME "rawImfile"
#define RAWIMFILE_INDEX_NAME "position"
#define P1PENDINGEXP_TABLE_NAME "p1PendingExp"
#define P1PENDINGEXP_INDEX_NAME "position"
#define P2PENDINGEXP_TABLE_NAME "p2PendingExp"
#define P2PENDINGEXP_INDEX_NAME "position"
#define P2PENDINGIMFILE_TABLE_NAME "p2PendingImfile"
#define P2PENDINGIMFILE_INDEX_NAME "position"
#define P2DONEEXP_TABLE_NAME "p2DoneExp"
#define P2DONEEXP_INDEX_NAME "position"
#define P2DONEIMFILE_TABLE_NAME "p2DoneImfile"
#define P2DONEIMFILE_INDEX_NAME "position"
#define P3PENDINGEXP_TABLE_NAME "p3PendingExp"
#define P3PENDINGEXP_INDEX_NAME "position"
#define DETRUN_TABLE_NAME "detRun"
#define DETRUN_INDEX_NAME "position"
#define DETINPUTEXP_TABLE_NAME "detInputExp"
#define DETINPUTEXP_INDEX_NAME "position"
#define DETPROCESSEDIMFILE_TABLE_NAME "detProcessedImfile"
#define DETPROCESSEDIMFILE_INDEX_NAME "position"
#define DETSTACKEDIMFILE_TABLE_NAME "detStackedImfile"
#define DETSTACKEDIMFILE_INDEX_NAME "position"
#define DETNORMALIZEDIMFILE_TABLE_NAME "detNormalizedImfile"
#define DETNORMALIZEDIMFILE_INDEX_NAME "position"
#define DETMASTERFRAME_TABLE_NAME "detMasterFrame"
#define DETMASTERFRAME_INDEX_NAME "position"
#define DETMASTERIMFILE_TABLE_NAME "detMasterImfile"
#define DETMASTERIMFILE_INDEX_NAME "position"
#define DETRESIDIMFILEANALYSIS_TABLE_NAME "detResidImfileAnalysis"
#define DETRESIDIMFILEANALYSIS_INDEX_NAME "position"
#define DETRESIDEXPANALYSIS_TABLE_NAME "detResidExpAnalysis"
#define DETRESIDEXPANALYSIS_INDEX_NAME "position"
#define MAX_STRING_LENGTH 1024

psDB *ippdbInit(const char *host, const char *user, const char *passwd, const char *dbname)
{
    return psDBInit(host, user, passwd, dbname);
}

void ippdbCleanup(psDB *dbh)
{
    psDBCleanup(dbh);
}

static void weatherRowFree(weatherRow *object);

weatherRow *weatherRowAlloc(psF32 temp01, psF32 humi01, psF32 temp02, psF32 humi02, psF32 temp03, psF32 humi03, psF32 pressure)
{
    weatherRow      *object;

    object = psAlloc(sizeof(weatherRow));
    psMemSetDeallocator(object, (psFreeFunc)weatherRowFree);

    object->temp01 = temp01;
    object->humi01 = humi01;
    object->temp02 = temp02;
    object->humi02 = humi02;
    object->temp03 = temp03;
    object->humi03 = humi03;
    object->pressure = pressure;

    return object;
}

static void weatherRowFree(weatherRow *object)
{
}

bool weatherCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, WEATHER_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", WEATHER_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp01");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi01");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp02");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi02");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp03");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi03");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item pressure");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, WEATHER_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool weatherDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, WEATHER_TABLE_NAME);
}

bool weatherInsert(psDB * dbh, psF32 temp01, psF32 humi01, psF32 temp02, psF32 humi02, psF32 temp03, psF32 humi03, psF32 pressure)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, temp01)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp01");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, humi01)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi01");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, temp02)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp02");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, humi02)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi02");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, temp03)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp03");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, humi03)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi03");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, pressure)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item pressure");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, WEATHER_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool weatherPop(psDB *dbh, psF32 *temp01, psF32 *humi01, psF32 *temp02, psF32 *humi02, psF32 *temp03, psF32 *humi03, psF32 *pressure)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, WEATHER_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", WEATHER_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, WEATHER_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", WEATHER_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, WEATHER_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, WEATHER_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);

    *temp01 = psMetadataLookupF32(&status, row, "temp01");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp01");
        psFree(row);
        return false;
    }
    *humi01 = psMetadataLookupF32(&status, row, "humi01");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi01");
        psFree(row);
        return false;
    }
    *temp02 = psMetadataLookupF32(&status, row, "temp02");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp02");
        psFree(row);
        return false;
    }
    *humi02 = psMetadataLookupF32(&status, row, "humi02");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi02");
        psFree(row);
        return false;
    }
    *temp03 = psMetadataLookupF32(&status, row, "temp03");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp03");
        psFree(row);
        return false;
    }
    *humi03 = psMetadataLookupF32(&status, row, "humi03");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi03");
        psFree(row);
        return false;
    }
    *pressure = psMetadataLookupF32(&status, row, "pressure");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item pressure");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool weatherInsertObject(psDB *dbh, weatherRow *object)
{
    return weatherInsert(dbh, object->temp01, object->humi01, object->temp02, object->humi02, object->temp03, object->humi03, object->pressure);
}

weatherRow *weatherPopObject(psDB *dbh)
{
    psF32           temp01;
    psF32           humi01;
    psF32           temp02;
    psF32           humi02;
    psF32           temp03;
    psF32           humi03;
    psF32           pressure;

    if (!weatherPop(dbh, &temp01, &humi01, &temp02, &humi02, &temp03, &humi03, &pressure)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return weatherRowAlloc(temp01, humi01, temp02, humi02, temp03, humi03, pressure);
}

bool weatherInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  WEATHER_TABLE_NAME
    if (!psFitsMoveExtName(fits, WEATHER_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", WEATHER_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, WEATHER_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool weatherPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!weatherSelectRowsFits(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",
                WEATHER_TABLE_NAME, WEATHER_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool weatherSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, WEATHER_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, WEATHER_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", WEATHER_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, WEATHER_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *weatherMetadataFromObject(const weatherRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, object->temp01)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp01");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, object->humi01)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi01");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, object->temp02)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp02");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, object->humi02)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi02");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, object->temp03)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item temp03");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, object->humi03)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item humi03");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, object->pressure)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item pressure");
        psFree(md);
        return NULL;
    }

    return md;
}

weatherRow *weatherObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psF32           temp01;
    psF32           humi01;
    psF32           temp02;
    psF32           humi02;
    psF32           temp03;
    psF32           humi03;
    psF32           pressure;

    temp01 = psMetadataLookupF32(&status, md, "temp01");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp01");
        return false;
    }
    humi01 = psMetadataLookupF32(&status, md, "humi01");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi01");
        return false;
    }
    temp02 = psMetadataLookupF32(&status, md, "temp02");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp02");
        return false;
    }
    humi02 = psMetadataLookupF32(&status, md, "humi02");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi02");
        return false;
    }
    temp03 = psMetadataLookupF32(&status, md, "temp03");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp03");
        return false;
    }
    humi03 = psMetadataLookupF32(&status, md, "humi03");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi03");
        return false;
    }
    pressure = psMetadataLookupF32(&status, md, "pressure");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item pressure");
        return false;
    }

    return weatherRowAlloc(temp01, humi01, temp02, humi02, temp03, humi03, pressure);
}
psArray *weatherSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, WEATHER_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, WEATHER_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", WEATHER_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++) {
        psArrayAdd(returnSet, 0, weatherObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long weatherDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        weatherRow *object = objects->data[i];
        psMetadata *where = weatherMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, WEATHER_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from weather");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void skyp_transparencyRowFree(skyp_transparencyRow *object);

skyp_transparencyRow *skyp_transparencyRowAlloc(const char *filter, psF64 trans, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
{
    skyp_transparencyRow *object;

    object = psAlloc(sizeof(skyp_transparencyRow));
    psMemSetDeallocator(object, (psFreeFunc)skyp_transparencyRowFree);

    object->filter = psStringCopy(filter);
    object->trans = trans;
    object->nstars = nstars;
    object->ra = ra;
    object->decl = decl;
    object->exptime = exptime;
    object->sky_bright = sky_bright;

    return object;
}

static void skyp_transparencyRowFree(skyp_transparencyRow *object)
{
    psFree(object->filter);
}

bool skyp_transparencyCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_TRANSPARENCY_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_TRANSPARENCY_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item trans");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, SKYP_TRANSPARENCY_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool skyp_transparencyDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, SKYP_TRANSPARENCY_TABLE_NAME);
}

bool skyp_transparencyInsert(psDB * dbh, const char *filter, psF64 trans, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, trans)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item trans");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, nstars)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, SKYP_TRANSPARENCY_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool skyp_transparencyPop(psDB *dbh, char **filter, psF64 *trans, psS32 *nstars, psF64 *ra, psF64 *decl, psF32 *exptime, psF64 *sky_bright)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_TRANSPARENCY_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, SKYP_TRANSPARENCY_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_TRANSPARENCY_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_TRANSPARENCY_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, SKYP_TRANSPARENCY_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);

    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *trans = psMetadataLookupF64(&status, row, "trans");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item trans");
        psFree(row);
        return false;
    }
    *nstars = psMetadataLookupS32(&status, row, "nstars");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
        psFree(row);
        return false;
    }
    *ra = psMetadataLookupF64(&status, row, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        psFree(row);
        return false;
    }
    *decl = psMetadataLookupF64(&status, row, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        psFree(row);
        return false;
    }
    *exptime = psMetadataLookupF32(&status, row, "exptime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
        psFree(row);
        return false;
    }
    *sky_bright = psMetadataLookupF64(&status, row, "sky_bright");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool skyp_transparencyInsertObject(psDB *dbh, skyp_transparencyRow *object)
{
    return skyp_transparencyInsert(dbh, object->filter, object->trans, object->nstars, object->ra, object->decl, object->exptime, object->sky_bright);
}

skyp_transparencyRow *skyp_transparencyPopObject(psDB *dbh)
{
    char            filter[256];
    psF64           trans;
    psS32           nstars;
    psF64           ra;
    psF64           decl;
    psF32           exptime;
    psF64           sky_bright;

    if (!skyp_transparencyPop(dbh, (char **)&filter, &trans, &nstars, &ra, &decl, &exptime, &sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return skyp_transparencyRowAlloc(filter, trans, nstars, ra, decl, exptime, sky_bright);
}

bool skyp_transparencyInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  SKYP_TRANSPARENCY_TABLE_NAME
    if (!psFitsMoveExtName(fits, SKYP_TRANSPARENCY_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_TRANSPARENCY_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, SKYP_TRANSPARENCY_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool skyp_transparencyPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!skyp_transparencySelectRowsFits(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",
                SKYP_TRANSPARENCY_TABLE_NAME, SKYP_TRANSPARENCY_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool skyp_transparencySelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_TRANSPARENCY_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, SKYP_TRANSPARENCY_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_TRANSPARENCY_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_TRANSPARENCY_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *skyp_transparencyMetadataFromObject(const skyp_transparencyRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, object->trans)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item trans");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, object->nstars)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, object->exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, object->sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return NULL;
    }

    return md;
}

skyp_transparencyRow *skyp_transparencyObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *filter;
    psF64           trans;
    psS32           nstars;
    psF64           ra;
    psF64           decl;
    psF32           exptime;
    psF64           sky_bright;

    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    trans = psMetadataLookupF64(&status, md, "trans");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item trans");
        return false;
    }
    nstars = psMetadataLookupS32(&status, md, "nstars");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
        return false;
    }
    ra = psMetadataLookupF64(&status, md, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        return false;
    }
    decl = psMetadataLookupF64(&status, md, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        return false;
    }
    exptime = psMetadataLookupF32(&status, md, "exptime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
        return false;
    }
    sky_bright = psMetadataLookupF64(&status, md, "sky_bright");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
        return false;
    }

    return skyp_transparencyRowAlloc(filter, trans, nstars, ra, decl, exptime, sky_bright);
}
psArray *skyp_transparencySelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_TRANSPARENCY_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, SKYP_TRANSPARENCY_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_TRANSPARENCY_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++) {
        psArrayAdd(returnSet, 0, skyp_transparencyObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long skyp_transparencyDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        skyp_transparencyRow *object = objects->data[i];
        psMetadata *where = skyp_transparencyMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_transparency");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void skyp_absorptionRowFree(skyp_absorptionRow *object);

skyp_absorptionRow *skyp_absorptionRowAlloc(const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
{
    skyp_absorptionRow *object;

    object = psAlloc(sizeof(skyp_absorptionRow));
    psMemSetDeallocator(object, (psFreeFunc)skyp_absorptionRowFree);

    object->disperser_id = psStringCopy(disperser_id);
    object->atmcomp1 = atmcomp1;
    object->atmcomp2 = atmcomp2;
    object->atmcomp3 = atmcomp3;
    object->nstars = nstars;
    object->ra = ra;
    object->decl = decl;
    object->exptime = exptime;
    object->sky_bright = sky_bright;

    return object;
}

static void skyp_absorptionRowFree(skyp_absorptionRow *object)
{
    psFree(object->disperser_id);
}

bool skyp_absorptionCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_ABSORPTION_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_ABSORPTION_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, SKYP_ABSORPTION_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool skyp_absorptionDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, SKYP_ABSORPTION_TABLE_NAME);
}

bool skyp_absorptionInsert(psDB * dbh, const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(disperser_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, atmcomp1)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, atmcomp2)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, atmcomp3)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, nstars)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, SKYP_ABSORPTION_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool skyp_absorptionPop(psDB *dbh, char **disperser_id, psF32 *atmcomp1, psF32 *atmcomp2, psF32 *atmcomp3, psS32 *nstars, psF64 *ra, psF64 *decl, psF32 *exptime, psF64 *sky_bright)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, SKYP_ABSORPTION_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_ABSORPTION_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, SKYP_ABSORPTION_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_ABSORPTION_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_ABSORPTION_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, SKYP_ABSORPTION_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);

    *disperser_id = psMetadataLookupPtr(&status, row, "disperser_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
        psFree(row);
        return false;
    }
    *atmcomp1 = psMetadataLookupF32(&status, row, "atmcomp1");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
        psFree(row);
        return false;
    }
    *atmcomp2 = psMetadataLookupF32(&status, row, "atmcomp2");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
        psFree(row);
        return false;
    }
    *atmcomp3 = psMetadataLookupF32(&status, row, "atmcomp3");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
        psFree(row);
        return false;
    }
    *nstars = psMetadataLookupS32(&status, row, "nstars");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
        psFree(row);
        return false;
    }
    *ra = psMetadataLookupF64(&status, row, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        psFree(row);
        return false;
    }
    *decl = psMetadataLookupF64(&status, row, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        psFree(row);
        return false;
    }
    *exptime = psMetadataLookupF32(&status, row, "exptime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
        psFree(row);
        return false;
    }
    *sky_bright = psMetadataLookupF64(&status, row, "sky_bright");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool skyp_absorptionInsertObject(psDB *dbh, skyp_absorptionRow *object)
{
    return skyp_absorptionInsert(dbh, object->disperser_id, object->atmcomp1, object->atmcomp2, object->atmcomp3, object->nstars, object->ra, object->decl, object->exptime, object->sky_bright);
}

skyp_absorptionRow *skyp_absorptionPopObject(psDB *dbh)
{
    char            disperser_id[256];
    psF32           atmcomp1;
    psF32           atmcomp2;
    psF32           atmcomp3;
    psS32           nstars;
    psF64           ra;
    psF64           decl;
    psF32           exptime;
    psF64           sky_bright;

    if (!skyp_absorptionPop(dbh, (char **)&disperser_id, &atmcomp1, &atmcomp2, &atmcomp3, &nstars, &ra, &decl, &exptime, &sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return skyp_absorptionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, nstars, ra, decl, exptime, sky_bright);
}

bool skyp_absorptionInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  SKYP_ABSORPTION_TABLE_NAME
    if (!psFitsMoveExtName(fits, SKYP_ABSORPTION_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_ABSORPTION_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, SKYP_ABSORPTION_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool skyp_absorptionPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!skyp_absorptionSelectRowsFits(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",
                SKYP_ABSORPTION_TABLE_NAME, SKYP_ABSORPTION_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool skyp_absorptionSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_ABSORPTION_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, SKYP_ABSORPTION_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_ABSORPTION_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_ABSORPTION_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *skyp_absorptionMetadataFromObject(const skyp_absorptionRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(object->disperser_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, object->atmcomp1)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, object->atmcomp2)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, object->atmcomp3)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, object->nstars)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, object->exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, object->sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return NULL;
    }

    return md;
}

skyp_absorptionRow *skyp_absorptionObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *disperser_id;
    psF32           atmcomp1;
    psF32           atmcomp2;
    psF32           atmcomp3;
    psS32           nstars;
    psF64           ra;
    psF64           decl;
    psF32           exptime;
    psF64           sky_bright;

    disperser_id = psMetadataLookupPtr(&status, md, "disperser_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
        return false;
    }
    atmcomp1 = psMetadataLookupF32(&status, md, "atmcomp1");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
        return false;
    }
    atmcomp2 = psMetadataLookupF32(&status, md, "atmcomp2");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
        return false;
    }
    atmcomp3 = psMetadataLookupF32(&status, md, "atmcomp3");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
        return false;
    }
    nstars = psMetadataLookupS32(&status, md, "nstars");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
        return false;
    }
    ra = psMetadataLookupF64(&status, md, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        return false;
    }
    decl = psMetadataLookupF64(&status, md, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        return false;
    }
    exptime = psMetadataLookupF32(&status, md, "exptime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
        return false;
    }
    sky_bright = psMetadataLookupF64(&status, md, "sky_bright");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
        return false;
    }

    return skyp_absorptionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, nstars, ra, decl, exptime, sky_bright);
}
psArray *skyp_absorptionSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_ABSORPTION_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, SKYP_ABSORPTION_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_ABSORPTION_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++) {
        psArrayAdd(returnSet, 0, skyp_absorptionObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long skyp_absorptionDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        skyp_absorptionRow *object = objects->data[i];
        psMetadata *where = skyp_absorptionMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, SKYP_ABSORPTION_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_absorption");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void skyp_emissionRowFree(skyp_emissionRow *object);

skyp_emissionRow *skyp_emissionRowAlloc(const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psF32 continuum, psF32 exptime)
{
    skyp_emissionRow *object;

    object = psAlloc(sizeof(skyp_emissionRow));
    psMemSetDeallocator(object, (psFreeFunc)skyp_emissionRowFree);

    object->disperser_id = psStringCopy(disperser_id);
    object->atmcomp1 = atmcomp1;
    object->atmcomp2 = atmcomp2;
    object->atmcomp3 = atmcomp3;
    object->continuum = continuum;
    object->exptime = exptime;

    return object;
}

static void skyp_emissionRowFree(skyp_emissionRow *object)
{
    psFree(object->disperser_id);
}

bool skyp_emissionCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_EMISSION_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_EMISSION_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item continuum");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, SKYP_EMISSION_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool skyp_emissionDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, SKYP_EMISSION_TABLE_NAME);
}

bool skyp_emissionInsert(psDB * dbh, const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psF32 continuum, psF32 exptime)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(disperser_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, atmcomp1)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, atmcomp2)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, atmcomp3)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, continuum)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item continuum");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, SKYP_EMISSION_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool skyp_emissionPop(psDB *dbh, char **disperser_id, psF32 *atmcomp1, psF32 *atmcomp2, psF32 *atmcomp3, psF32 *continuum, psF32 *exptime)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, SKYP_EMISSION_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_EMISSION_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, SKYP_EMISSION_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_EMISSION_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_EMISSION_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, SKYP_EMISSION_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);

    *disperser_id = psMetadataLookupPtr(&status, row, "disperser_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
        psFree(row);
        return false;
    }
    *atmcomp1 = psMetadataLookupF32(&status, row, "atmcomp1");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
        psFree(row);
        return false;
    }
    *atmcomp2 = psMetadataLookupF32(&status, row, "atmcomp2");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
        psFree(row);
        return false;
    }
    *atmcomp3 = psMetadataLookupF32(&status, row, "atmcomp3");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
        psFree(row);
        return false;
    }
    *continuum = psMetadataLookupF32(&status, row, "continuum");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item continuum");
        psFree(row);
        return false;
    }
    *exptime = psMetadataLookupF32(&status, row, "exptime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool skyp_emissionInsertObject(psDB *dbh, skyp_emissionRow *object)
{
    return skyp_emissionInsert(dbh, object->disperser_id, object->atmcomp1, object->atmcomp2, object->atmcomp3, object->continuum, object->exptime);
}

skyp_emissionRow *skyp_emissionPopObject(psDB *dbh)
{
    char            disperser_id[256];
    psF32           atmcomp1;
    psF32           atmcomp2;
    psF32           atmcomp3;
    psF32           continuum;
    psF32           exptime;

    if (!skyp_emissionPop(dbh, (char **)&disperser_id, &atmcomp1, &atmcomp2, &atmcomp3, &continuum, &exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return skyp_emissionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, continuum, exptime);
}

bool skyp_emissionInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  SKYP_EMISSION_TABLE_NAME
    if (!psFitsMoveExtName(fits, SKYP_EMISSION_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_EMISSION_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, SKYP_EMISSION_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool skyp_emissionPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!skyp_emissionSelectRowsFits(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",
                SKYP_EMISSION_TABLE_NAME, SKYP_EMISSION_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool skyp_emissionSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_EMISSION_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, SKYP_EMISSION_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_EMISSION_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_EMISSION_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *skyp_emissionMetadataFromObject(const skyp_emissionRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(object->disperser_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, object->atmcomp1)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, object->atmcomp2)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, object->atmcomp3)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, object->continuum)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item continuum");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, object->exptime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
        psFree(md);
        return NULL;
    }

    return md;
}

skyp_emissionRow *skyp_emissionObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *disperser_id;
    psF32           atmcomp1;
    psF32           atmcomp2;
    psF32           atmcomp3;
    psF32           continuum;
    psF32           exptime;

    disperser_id = psMetadataLookupPtr(&status, md, "disperser_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
        return false;
    }
    atmcomp1 = psMetadataLookupF32(&status, md, "atmcomp1");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
        return false;
    }
    atmcomp2 = psMetadataLookupF32(&status, md, "atmcomp2");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
        return false;
    }
    atmcomp3 = psMetadataLookupF32(&status, md, "atmcomp3");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
        return false;
    }
    continuum = psMetadataLookupF32(&status, md, "continuum");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item continuum");
        return false;
    }
    exptime = psMetadataLookupF32(&status, md, "exptime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
        return false;
    }

    return skyp_emissionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, continuum, exptime);
}
psArray *skyp_emissionSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_EMISSION_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, SKYP_EMISSION_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_EMISSION_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++) {
        psArrayAdd(returnSet, 0, skyp_emissionObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long skyp_emissionDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        skyp_emissionRow *object = objects->data[i];
        psMetadata *where = skyp_emissionMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, SKYP_EMISSION_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_emission");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void dimmRowFree(dimmRow *object);

dimmRow *dimmRowAlloc(psF32 sigmax, psF32 sigmay, psF32 fwhm, psF64 ra, psF64 decl, psF32 expttime, const char *telescope_id)
{
    dimmRow         *object;

    object = psAlloc(sizeof(dimmRow));
    psMemSetDeallocator(object, (psFreeFunc)dimmRowFree);

    object->sigmax = sigmax;
    object->sigmay = sigmay;
    object->fwhm = fwhm;
    object->ra = ra;
    object->decl = decl;
    object->expttime = expttime;
    object->telescope_id = psStringCopy(telescope_id);

    return object;
}

static void dimmRowFree(dimmRow *object)
{
    psFree(object->telescope_id);
}

bool dimmCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DIMM_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DIMM_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmax");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmay");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fwhm");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item expttime");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope_id");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, DIMM_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool dimmDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DIMM_TABLE_NAME);
}

bool dimmInsert(psDB * dbh, psF32 sigmax, psF32 sigmay, psF32 fwhm, psF64 ra, psF64 decl, psF32 expttime, const char *telescope_id)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, sigmax)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmax");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, sigmay)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmay");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, fwhm)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fwhm");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, expttime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item expttime");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, psStringCopy(telescope_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope_id");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DIMM_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool dimmPop(psDB *dbh, psF32 *sigmax, psF32 *sigmay, psF32 *fwhm, psF64 *ra, psF64 *decl, psF32 *expttime, char **telescope_id)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DIMM_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DIMM_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, DIMM_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DIMM_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DIMM_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DIMM_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);

    *sigmax = psMetadataLookupF32(&status, row, "sigmax");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmax");
        psFree(row);
        return false;
    }
    *sigmay = psMetadataLookupF32(&status, row, "sigmay");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmay");
        psFree(row);
        return false;
    }
    *fwhm = psMetadataLookupF32(&status, row, "fwhm");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fwhm");
        psFree(row);
        return false;
    }
    *ra = psMetadataLookupF64(&status, row, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        psFree(row);
        return false;
    }
    *decl = psMetadataLookupF64(&status, row, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        psFree(row);
        return false;
    }
    *expttime = psMetadataLookupF32(&status, row, "expttime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item expttime");
        psFree(row);
        return false;
    }
    *telescope_id = psMetadataLookupPtr(&status, row, "telescope_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope_id");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool dimmInsertObject(psDB *dbh, dimmRow *object)
{
    return dimmInsert(dbh, object->sigmax, object->sigmay, object->fwhm, object->ra, object->decl, object->expttime, object->telescope_id);
}

dimmRow *dimmPopObject(psDB *dbh)
{
    psF32           sigmax;
    psF32           sigmay;
    psF32           fwhm;
    psF64           ra;
    psF64           decl;
    psF32           expttime;
    char            telescope_id[256];

    if (!dimmPop(dbh, &sigmax, &sigmay, &fwhm, &ra, &decl, &expttime, (char **)&telescope_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return dimmRowAlloc(sigmax, sigmay, fwhm, ra, decl, expttime, telescope_id);
}

bool dimmInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DIMM_TABLE_NAME
    if (!psFitsMoveExtName(fits, DIMM_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DIMM_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, DIMM_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool dimmPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!dimmSelectRowsFits(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",
                DIMM_TABLE_NAME, DIMM_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool dimmSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DIMM_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, DIMM_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DIMM_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DIMM_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *dimmMetadataFromObject(const dimmRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, object->sigmax)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmax");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, object->sigmay)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmay");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, object->fwhm)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fwhm");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, object->expttime)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item expttime");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, psStringCopy(object->telescope_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope_id");
        psFree(md);
        return NULL;
    }

    return md;
}

dimmRow *dimmObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psF32           sigmax;
    psF32           sigmay;
    psF32           fwhm;
    psF64           ra;
    psF64           decl;
    psF32           expttime;
    char            *telescope_id;

    sigmax = psMetadataLookupF32(&status, md, "sigmax");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmax");
        return false;
    }
    sigmay = psMetadataLookupF32(&status, md, "sigmay");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmay");
        return false;
    }
    fwhm = psMetadataLookupF32(&status, md, "fwhm");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fwhm");
        return false;
    }
    ra = psMetadataLookupF64(&status, md, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        return false;
    }
    decl = psMetadataLookupF64(&status, md, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        return false;
    }
    expttime = psMetadataLookupF32(&status, md, "expttime");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item expttime");
        return false;
    }
    telescope_id = psMetadataLookupPtr(&status, md, "telescope_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope_id");
        return false;
    }

    return dimmRowAlloc(sigmax, sigmay, fwhm, ra, decl, expttime, telescope_id);
}
psArray *dimmSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DIMM_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, DIMM_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DIMM_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++) {
        psArrayAdd(returnSet, 0, dimmObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long dimmDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        dimmRow *object = objects->data[i];
        psMetadata *where = dimmMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DIMM_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from dimm");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void skyp_irRowFree(skyp_irRow *object);

skyp_irRow *skyp_irRowAlloc(psF64 sky_bright, psF64 sky_var, psF64 ra, psF64 decl, psF32 fov_x, psF32 fov_y)
{
    skyp_irRow      *object;

    object = psAlloc(sizeof(skyp_irRow));
    psMemSetDeallocator(object, (psFreeFunc)skyp_irRowFree);

    object->sky_bright = sky_bright;
    object->sky_var = sky_var;
    object->ra = ra;
    object->decl = decl;
    object->fov_x = fov_x;
    object->fov_y = fov_y;

    return object;
}

static void skyp_irRowFree(skyp_irRow *object)
{
}

bool skyp_irCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_IR_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_IR_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_var");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_x");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_y");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, SKYP_IR_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool skyp_irDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, SKYP_IR_TABLE_NAME);
}

bool skyp_irInsert(psDB * dbh, psF64 sky_bright, psF64 sky_var, psF64 ra, psF64 decl, psF32 fov_x, psF32 fov_y)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, sky_var)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_var");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, fov_x)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_x");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, fov_y)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_y");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, SKYP_IR_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool skyp_irPop(psDB *dbh, psF64 *sky_bright, psF64 *sky_var, psF64 *ra, psF64 *decl, psF32 *fov_x, psF32 *fov_y)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, SKYP_IR_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_IR_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, SKYP_IR_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_IR_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_IR_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, SKYP_IR_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);

    *sky_bright = psMetadataLookupF64(&status, row, "sky_bright");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
        psFree(row);
        return false;
    }
    *sky_var = psMetadataLookupF64(&status, row, "sky_var");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_var");
        psFree(row);
        return false;
    }
    *ra = psMetadataLookupF64(&status, row, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        psFree(row);
        return false;
    }
    *decl = psMetadataLookupF64(&status, row, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        psFree(row);
        return false;
    }
    *fov_x = psMetadataLookupF32(&status, row, "fov_x");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_x");
        psFree(row);
        return false;
    }
    *fov_y = psMetadataLookupF32(&status, row, "fov_y");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_y");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool skyp_irInsertObject(psDB *dbh, skyp_irRow *object)
{
    return skyp_irInsert(dbh, object->sky_bright, object->sky_var, object->ra, object->decl, object->fov_x, object->fov_y);
}

skyp_irRow *skyp_irPopObject(psDB *dbh)
{
    psF64           sky_bright;
    psF64           sky_var;
    psF64           ra;
    psF64           decl;
    psF32           fov_x;
    psF32           fov_y;

    if (!skyp_irPop(dbh, &sky_bright, &sky_var, &ra, &decl, &fov_x, &fov_y)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return skyp_irRowAlloc(sky_bright, sky_var, ra, decl, fov_x, fov_y);
}

bool skyp_irInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  SKYP_IR_TABLE_NAME
    if (!psFitsMoveExtName(fits, SKYP_IR_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_IR_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, SKYP_IR_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool skyp_irPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!skyp_irSelectRowsFits(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",
                SKYP_IR_TABLE_NAME, SKYP_IR_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool skyp_irSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_IR_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, SKYP_IR_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_IR_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_IR_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *skyp_irMetadataFromObject(const skyp_irRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, object->sky_bright)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, object->sky_var)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_var");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, object->fov_x)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_x");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, object->fov_y)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_y");
        psFree(md);
        return NULL;
    }

    return md;
}

skyp_irRow *skyp_irObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psF64           sky_bright;
    psF64           sky_var;
    psF64           ra;
    psF64           decl;
    psF32           fov_x;
    psF32           fov_y;

    sky_bright = psMetadataLookupF64(&status, md, "sky_bright");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
        return false;
    }
    sky_var = psMetadataLookupF64(&status, md, "sky_var");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_var");
        return false;
    }
    ra = psMetadataLookupF64(&status, md, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        return false;
    }
    decl = psMetadataLookupF64(&status, md, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        return false;
    }
    fov_x = psMetadataLookupF32(&status, md, "fov_x");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_x");
        return false;
    }
    fov_y = psMetadataLookupF32(&status, md, "fov_y");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_y");
        return false;
    }

    return skyp_irRowAlloc(sky_bright, sky_var, ra, decl, fov_x, fov_y);
}
psArray *skyp_irSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SKYP_IR_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, SKYP_IR_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_IR_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++) {
        psArrayAdd(returnSet, 0, skyp_irObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long skyp_irDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        skyp_irRow *object = objects->data[i];
        psMetadata *where = skyp_irMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, SKYP_IR_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_ir");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void domeRowFree(domeRow *object);

domeRow *domeRowAlloc(psF32 az, bool open, bool light, bool track)
{
    domeRow         *object;

    object = psAlloc(sizeof(domeRow));
    psMemSetDeallocator(object, (psFreeFunc)domeRowFree);

    object->az = az;
    object->open = open;
    object->light = light;
    object->track = track;

    return object;
}

static void domeRowFree(domeRow *object)
{
}

bool domeCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DOME_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DOME_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, true)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item open");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, true)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item light");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, true)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item track");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, DOME_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool domeDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DOME_TABLE_NAME);
}

bool domeInsert(psDB * dbh, psF32 az, bool open, bool light, bool track)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, az)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, open)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item open");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, light)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item light");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, track)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item track");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DOME_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool domePop(psDB *dbh, psF32 *az, bool *open, bool *light, bool *track)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DOME_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DOME_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, DOME_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DOME_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DOME_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DOME_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);

    *az = psMetadataLookupF32(&status, row, "az");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
        psFree(row);
        return false;
    }
    *open = psMetadataLookupBool(&status, row, "open");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item open");
        psFree(row);
        return false;
    }
    *light = psMetadataLookupBool(&status, row, "light");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item light");
        psFree(row);
        return false;
    }
    *track = psMetadataLookupBool(&status, row, "track");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item track");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool domeInsertObject(psDB *dbh, domeRow *object)
{
    return domeInsert(dbh, object->az, object->open, object->light, object->track);
}

domeRow *domePopObject(psDB *dbh)
{
    psF32           az;
    bool            open;
    bool            light;
    bool            track;

    if (!domePop(dbh, &az, &open, &light, &track)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return domeRowAlloc(az, open, light, track);
}

bool domeInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DOME_TABLE_NAME
    if (!psFitsMoveExtName(fits, DOME_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DOME_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, DOME_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool domePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!domeSelectRowsFits(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",
                DOME_TABLE_NAME, DOME_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool domeSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DOME_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, DOME_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DOME_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DOME_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *domeMetadataFromObject(const domeRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, object->az)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, object->open)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item open");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, object->light)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item light");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, object->track)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item track");
        psFree(md);
        return NULL;
    }

    return md;
}

domeRow *domeObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psF32           az;
    bool            open;
    bool            light;
    bool            track;

    az = psMetadataLookupF32(&status, md, "az");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
        return false;
    }
    open = psMetadataLookupBool(&status, md, "open");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item open");
        return false;
    }
    light = psMetadataLookupBool(&status, md, "light");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item light");
        return false;
    }
    track = psMetadataLookupBool(&status, md, "track");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item track");
        return false;
    }

    return domeRowAlloc(az, open, light, track);
}
psArray *domeSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DOME_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, DOME_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DOME_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++) {
        psArrayAdd(returnSet, 0, domeObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long domeDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        domeRow *object = objects->data[i];
        psMetadata *where = domeMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DOME_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from dome");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void telescopeRowFree(telescopeRow *object);

telescopeRow *telescopeRowAlloc(const char *guide, psF32 alt, psF32 az, psF64 ra, psF64 decl)
{
    telescopeRow    *object;

    object = psAlloc(sizeof(telescopeRow));
    psMemSetDeallocator(object, (psFreeFunc)telescopeRowFree);

    object->guide = psStringCopy(guide);
    object->alt = alt;
    object->az = az;
    object->ra = ra;
    object->decl = decl;

    return object;
}

static void telescopeRowFree(telescopeRow *object)
{
    psFree(object->guide);
}

bool telescopeCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, TELESCOPE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", TELESCOPE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item guide");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item alt");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, TELESCOPE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool telescopeDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, TELESCOPE_TABLE_NAME);
}

bool telescopeInsert(psDB * dbh, const char *guide, psF32 alt, psF32 az, psF64 ra, psF64 decl)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, psStringCopy(guide))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item guide");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, alt)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item alt");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, az)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return false;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, TELESCOPE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool telescopePop(psDB *dbh, char **guide, psF32 *alt, psF32 *az, psF64 *ra, psF64 *decl)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, TELESCOPE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", TELESCOPE_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, TELESCOPE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", TELESCOPE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, TELESCOPE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, TELESCOPE_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);

    *guide = psMetadataLookupPtr(&status, row, "guide");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item guide");
        psFree(row);
        return false;
    }
    *alt = psMetadataLookupF32(&status, row, "alt");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item alt");
        psFree(row);
        return false;
    }
    *az = psMetadataLookupF32(&status, row, "az");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
        psFree(row);
        return false;
    }
    *ra = psMetadataLookupF64(&status, row, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        psFree(row);
        return false;
    }
    *decl = psMetadataLookupF64(&status, row, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool telescopeInsertObject(psDB *dbh, telescopeRow *object)
{
    return telescopeInsert(dbh, object->guide, object->alt, object->az, object->ra, object->decl);
}

telescopeRow *telescopePopObject(psDB *dbh)
{
    char            guide[256];
    psF32           alt;
    psF32           az;
    psF64           ra;
    psF64           decl;

    if (!telescopePop(dbh, (char **)&guide, &alt, &az, &ra, &decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return telescopeRowAlloc(guide, alt, az, ra, decl);
}

bool telescopeInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  TELESCOPE_TABLE_NAME
    if (!psFitsMoveExtName(fits, TELESCOPE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", TELESCOPE_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, TELESCOPE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool telescopePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!telescopeSelectRowsFits(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",
                TELESCOPE_TABLE_NAME, TELESCOPE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool telescopeSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, TELESCOPE_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, TELESCOPE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", TELESCOPE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, TELESCOPE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *telescopeMetadataFromObject(const telescopeRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, psStringCopy(object->guide))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item guide");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, object->alt)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item alt");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, object->az)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
        psFree(md);
        return NULL;
    }

    return md;
}

telescopeRow *telescopeObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *guide;
    psF32           alt;
    psF32           az;
    psF64           ra;
    psF64           decl;

    guide = psMetadataLookupPtr(&status, md, "guide");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item guide");
        return false;
    }
    alt = psMetadataLookupF32(&status, md, "alt");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item alt");
        return false;
    }
    az = psMetadataLookupF32(&status, md, "az");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
        return false;
    }
    ra = psMetadataLookupF64(&status, md, "ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
        return false;
    }
    decl = psMetadataLookupF64(&status, md, "decl");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
        return false;
    }

    return telescopeRowAlloc(guide, alt, az, ra, decl);
}
psArray *telescopeSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, TELESCOPE_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, TELESCOPE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", TELESCOPE_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++) {
        psArrayAdd(returnSet, 0, telescopeObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long telescopeDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        telescopeRow *object = objects->data[i];
        psMetadata *where = telescopeMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, TELESCOPE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from telescope");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void summitExpRowFree(summitExpRow *object);

summitExpRow *summitExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, const char *uri)
{
    summitExpRow    *object;

    object = psAlloc(sizeof(summitExpRow));
    psMemSetDeallocator(object, (psFreeFunc)summitExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->uri = psStringCopy(uri);

    return object;
}

static void summitExpRowFree(summitExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->uri);
}

bool summitExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, SUMMITEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SUMMITEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        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;
    }

    status = psDBCreateTable(dbh, SUMMITEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool summitExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, SUMMITEXP_TABLE_NAME);
}

bool summitExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, const char *uri)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, SUMMITEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool summitExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, char **uri)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, SUMMITEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SUMMITEXP_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, SUMMITEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SUMMITEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, SUMMITEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, SUMMITEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        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;
    }

    psFree(row);

    return true;
}

bool summitExpInsertObject(psDB *dbh, summitExpRow *object)
{
    return summitExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->uri);
}

summitExpRow *summitExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    char            uri[256];

    if (!summitExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, (char **)&uri)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return summitExpRowAlloc(exp_id, camera, telescope, exp_type, uri);
}

bool summitExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  SUMMITEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, SUMMITEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SUMMITEXP_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, SUMMITEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool summitExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!summitExpSelectRowsFits(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",
                SUMMITEXP_TABLE_NAME, SUMMITEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool summitExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SUMMITEXP_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, SUMMITEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SUMMITEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, SUMMITEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *summitExpMetadataFromObject(const summitExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }

    return md;
}

summitExpRow *summitExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    char            *uri;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }

    return summitExpRowAlloc(exp_id, camera, telescope, exp_type, uri);
}
psArray *summitExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, SUMMITEXP_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, SUMMITEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SUMMITEXP_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++) {
        psArrayAdd(returnSet, 0, summitExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long summitExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        summitExpRow *object = objects->data[i];
        psMetadata *where = summitExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, SUMMITEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from summitExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void pzPendingExpRowFree(pzPendingExpRow *object);

pzPendingExpRow *pzPendingExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
{
    pzPendingExpRow *object;

    object = psAlloc(sizeof(pzPendingExpRow));
    psMemSetDeallocator(object, (psFreeFunc)pzPendingExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;

    return object;
}

static void pzPendingExpRowFree(pzPendingExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
}

bool pzPendingExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, PZPENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", PZPENDINGEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, PZPENDINGEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool pzPendingExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, PZPENDINGEXP_TABLE_NAME);
}

bool pzPendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, PZPENDINGEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool pzPendingExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, PZPENDINGEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGEXP_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, PZPENDINGEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, PZPENDINGEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, PZPENDINGEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool pzPendingExpInsertObject(psDB *dbh, pzPendingExpRow *object)
{
    return pzPendingExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles);
}

pzPendingExpRow *pzPendingExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;

    if (!pzPendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return pzPendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
}

bool pzPendingExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  PZPENDINGEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, PZPENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", PZPENDINGEXP_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, PZPENDINGEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool pzPendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!pzPendingExpSelectRowsFits(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",
                PZPENDINGEXP_TABLE_NAME, PZPENDINGEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool pzPendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, PZPENDINGEXP_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, PZPENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, PZPENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *pzPendingExpMetadataFromObject(const pzPendingExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }

    return md;
}

pzPendingExpRow *pzPendingExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }

    return pzPendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
}
psArray *pzPendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, PZPENDINGEXP_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, PZPENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGEXP_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++) {
        psArrayAdd(returnSet, 0, pzPendingExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long pzPendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        pzPendingExpRow *object = objects->data[i];
        psMetadata *where = pzPendingExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, PZPENDINGEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from pzPendingExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void pzPendingImfileRowFree(pzPendingImfileRow *object);

pzPendingImfileRow *pzPendingImfileRowAlloc(const char *exp_id, psS32 bytes, const char *md5sum, const char *class, const char *class_id, const char *uri)
{
    pzPendingImfileRow *object;

    object = psAlloc(sizeof(pzPendingImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)pzPendingImfileRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->bytes = bytes;
    object->md5sum = psStringCopy(md5sum);
    object->class = psStringCopy(class);
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);

    return object;
}

static void pzPendingImfileRowFree(pzPendingImfileRow *object)
{
    psFree(object->exp_id);
    psFree(object->md5sum);
    psFree(object->class);
    psFree(object->class_id);
    psFree(object->uri);
}

bool pzPendingImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, PZPENDINGIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", PZPENDINGIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item bytes");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, "32")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item md5sum");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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;
    }

    status = psDBCreateTable(dbh, PZPENDINGIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool pzPendingImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, PZPENDINGIMFILE_TABLE_NAME);
}

bool pzPendingImfileInsert(psDB * dbh, const char *exp_id, psS32 bytes, const char *md5sum, const char *class, const char *class_id, const char *uri)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, bytes)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item bytes");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, psStringCopy(md5sum))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item md5sum");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(class))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, PZPENDINGIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool pzPendingImfilePop(psDB *dbh, char **exp_id, psS32 *bytes, char **md5sum, char **class, char **class_id, char **uri)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, PZPENDINGIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGIMFILE_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, PZPENDINGIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, PZPENDINGIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, PZPENDINGIMFILE_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *bytes = psMetadataLookupS32(&status, row, "bytes");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bytes");
        psFree(row);
        return false;
    }
    *md5sum = psMetadataLookupPtr(&status, row, "md5sum");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item md5sum");
        psFree(row);
        return false;
    }
    *class = psMetadataLookupPtr(&status, row, "class");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }

    psFree(row);

    return true;
}

bool pzPendingImfileInsertObject(psDB *dbh, pzPendingImfileRow *object)
{
    return pzPendingImfileInsert(dbh, object->exp_id, object->bytes, object->md5sum, object->class, object->class_id, object->uri);
}

pzPendingImfileRow *pzPendingImfilePopObject(psDB *dbh)
{
    char            exp_id[256];
    psS32           bytes;
    char            md5sum[256];
    char            class[256];
    char            class_id[256];
    char            uri[256];

    if (!pzPendingImfilePop(dbh, (char **)&exp_id, &bytes, (char **)&md5sum, (char **)&class, (char **)&class_id, (char **)&uri)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return pzPendingImfileRowAlloc(exp_id, bytes, md5sum, class, class_id, uri);
}

bool pzPendingImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  PZPENDINGIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, PZPENDINGIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", PZPENDINGIMFILE_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, PZPENDINGIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool pzPendingImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!pzPendingImfileSelectRowsFits(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",
                PZPENDINGIMFILE_TABLE_NAME, PZPENDINGIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool pzPendingImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, PZPENDINGIMFILE_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, PZPENDINGIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, PZPENDINGIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *pzPendingImfileMetadataFromObject(const pzPendingImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, object->bytes)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item bytes");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, psStringCopy(object->md5sum))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item md5sum");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(object->class))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }

    return md;
}

pzPendingImfileRow *pzPendingImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    psS32           bytes;
    char            *md5sum;
    char            *class;
    char            *class_id;
    char            *uri;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    bytes = psMetadataLookupS32(&status, md, "bytes");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bytes");
        return false;
    }
    md5sum = psMetadataLookupPtr(&status, md, "md5sum");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item md5sum");
        return false;
    }
    class = psMetadataLookupPtr(&status, md, "class");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }

    return pzPendingImfileRowAlloc(exp_id, bytes, md5sum, class, class_id, uri);
}
psArray *pzPendingImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, PZPENDINGIMFILE_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, PZPENDINGIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGIMFILE_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++) {
        psArrayAdd(returnSet, 0, pzPendingImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long pzPendingImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        pzPendingImfileRow *object = objects->data[i];
        psMetadata *where = pzPendingImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, PZPENDINGIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from pzPendingImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void newExpRowFree(newExpRow *object);

newExpRow *newExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
{
    newExpRow       *object;

    object = psAlloc(sizeof(newExpRow));
    psMemSetDeallocator(object, (psFreeFunc)newExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;

    return object;
}

static void newExpRowFree(newExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
}

bool newExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, NEWEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", NEWEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, NEWEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool newExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, NEWEXP_TABLE_NAME);
}

bool newExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, NEWEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool newExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, NEWEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWEXP_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, NEWEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, NEWEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, NEWEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool newExpInsertObject(psDB *dbh, newExpRow *object)
{
    return newExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles);
}

newExpRow *newExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;

    if (!newExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return newExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
}

bool newExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  NEWEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, NEWEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", NEWEXP_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, NEWEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool newExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!newExpSelectRowsFits(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",
                NEWEXP_TABLE_NAME, NEWEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool newExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, NEWEXP_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, NEWEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, NEWEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *newExpMetadataFromObject(const newExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }

    return md;
}

newExpRow *newExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }

    return newExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
}
psArray *newExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, NEWEXP_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, NEWEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWEXP_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++) {
        psArrayAdd(returnSet, 0, newExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long newExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        newExpRow *object = objects->data[i];
        psMetadata *where = newExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, NEWEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from newExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void newImfileRowFree(newImfileRow *object);

newImfileRow *newImfileRowAlloc(const char *exp_id, const char *class, const char *class_id, const char *uri)
{
    newImfileRow    *object;

    object = psAlloc(sizeof(newImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)newImfileRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->class = psStringCopy(class);
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);

    return object;
}

static void newImfileRowFree(newImfileRow *object)
{
    psFree(object->exp_id);
    psFree(object->class);
    psFree(object->class_id);
    psFree(object->uri);
}

bool newImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, NEWIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", NEWIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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;
    }

    status = psDBCreateTable(dbh, NEWIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool newImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, NEWIMFILE_TABLE_NAME);
}

bool newImfileInsert(psDB * dbh, const char *exp_id, const char *class, const char *class_id, const char *uri)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(class))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, NEWIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool newImfilePop(psDB *dbh, char **exp_id, char **class, char **class_id, char **uri)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, NEWIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWIMFILE_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, NEWIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, NEWIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, NEWIMFILE_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *class = psMetadataLookupPtr(&status, row, "class");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }

    psFree(row);

    return true;
}

bool newImfileInsertObject(psDB *dbh, newImfileRow *object)
{
    return newImfileInsert(dbh, object->exp_id, object->class, object->class_id, object->uri);
}

newImfileRow *newImfilePopObject(psDB *dbh)
{
    char            exp_id[256];
    char            class[256];
    char            class_id[256];
    char            uri[256];

    if (!newImfilePop(dbh, (char **)&exp_id, (char **)&class, (char **)&class_id, (char **)&uri)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return newImfileRowAlloc(exp_id, class, class_id, uri);
}

bool newImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  NEWIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, NEWIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", NEWIMFILE_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, NEWIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool newImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!newImfileSelectRowsFits(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",
                NEWIMFILE_TABLE_NAME, NEWIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool newImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, NEWIMFILE_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, NEWIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, NEWIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *newImfileMetadataFromObject(const newImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(object->class))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }

    return md;
}

newImfileRow *newImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *class;
    char            *class_id;
    char            *uri;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    class = psMetadataLookupPtr(&status, md, "class");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }

    return newImfileRowAlloc(exp_id, class, class_id, uri);
}
psArray *newImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, NEWIMFILE_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, NEWIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWIMFILE_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++) {
        psArrayAdd(returnSet, 0, newImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long newImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        newImfileRow *object = objects->data[i];
        psMetadata *where = newImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, NEWIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from newImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void rawDetrendExpRowFree(rawDetrendExpRow *object);

rawDetrendExpRow *rawDetrendExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
{
    rawDetrendExpRow *object;

    object = psAlloc(sizeof(rawDetrendExpRow));
    psMemSetDeallocator(object, (psFreeFunc)rawDetrendExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);

    return object;
}

static void rawDetrendExpRowFree(rawDetrendExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
}

bool rawDetrendExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, RAWDETRENDEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", RAWDETRENDEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, RAWDETRENDEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool rawDetrendExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, RAWDETRENDEXP_TABLE_NAME);
}

bool rawDetrendExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, RAWDETRENDEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool rawDetrendExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, RAWDETRENDEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWDETRENDEXP_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, RAWDETRENDEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWDETRENDEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, RAWDETRENDEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, RAWDETRENDEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool rawDetrendExpInsertObject(psDB *dbh, rawDetrendExpRow *object)
{
    return rawDetrendExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats);
}

rawDetrendExpRow *rawDetrendExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];

    if (!rawDetrendExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return rawDetrendExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
}

bool rawDetrendExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  RAWDETRENDEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, RAWDETRENDEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", RAWDETRENDEXP_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, RAWDETRENDEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool rawDetrendExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!rawDetrendExpSelectRowsFits(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",
                RAWDETRENDEXP_TABLE_NAME, RAWDETRENDEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool rawDetrendExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, RAWDETRENDEXP_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, RAWDETRENDEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWDETRENDEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, RAWDETRENDEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *rawDetrendExpMetadataFromObject(const rawDetrendExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }

    return md;
}

rawDetrendExpRow *rawDetrendExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }

    return rawDetrendExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
}
psArray *rawDetrendExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, RAWDETRENDEXP_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, RAWDETRENDEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWDETRENDEXP_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++) {
        psArrayAdd(returnSet, 0, rawDetrendExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long rawDetrendExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        rawDetrendExpRow *object = objects->data[i];
        psMetadata *where = rawDetrendExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, RAWDETRENDEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from rawDetrendExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void rawScienceExpRowFree(rawScienceExpRow *object);

rawScienceExpRow *rawScienceExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
{
    rawScienceExpRow *object;

    object = psAlloc(sizeof(rawScienceExpRow));
    psMemSetDeallocator(object, (psFreeFunc)rawScienceExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);

    return object;
}

static void rawScienceExpRowFree(rawScienceExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
}

bool rawScienceExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, RAWSCIENCEEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", RAWSCIENCEEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, RAWSCIENCEEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool rawScienceExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, RAWSCIENCEEXP_TABLE_NAME);
}

bool rawScienceExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, RAWSCIENCEEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool rawScienceExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, RAWSCIENCEEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWSCIENCEEXP_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, RAWSCIENCEEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWSCIENCEEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, RAWSCIENCEEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, RAWSCIENCEEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool rawScienceExpInsertObject(psDB *dbh, rawScienceExpRow *object)
{
    return rawScienceExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats);
}

rawScienceExpRow *rawScienceExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];

    if (!rawScienceExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return rawScienceExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
}

bool rawScienceExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  RAWSCIENCEEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, RAWSCIENCEEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", RAWSCIENCEEXP_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, RAWSCIENCEEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool rawScienceExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!rawScienceExpSelectRowsFits(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",
                RAWSCIENCEEXP_TABLE_NAME, RAWSCIENCEEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool rawScienceExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, RAWSCIENCEEXP_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, RAWSCIENCEEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWSCIENCEEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, RAWSCIENCEEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *rawScienceExpMetadataFromObject(const rawScienceExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }

    return md;
}

rawScienceExpRow *rawScienceExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }

    return rawScienceExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
}
psArray *rawScienceExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, RAWSCIENCEEXP_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, RAWSCIENCEEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWSCIENCEEXP_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++) {
        psArrayAdd(returnSet, 0, rawScienceExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long rawScienceExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        rawScienceExpRow *object = objects->data[i];
        psMetadata *where = rawScienceExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, RAWSCIENCEEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from rawScienceExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void rawImfileRowFree(rawImfileRow *object);

rawImfileRow *rawImfileRowAlloc(const char *exp_id, const char *class_id, const char *uri, const char *stats)
{
    rawImfileRow    *object;

    object = psAlloc(sizeof(rawImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)rawImfileRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);

    return object;
}

static void rawImfileRowFree(rawImfileRow *object)
{
    psFree(object->exp_id);
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
}

bool rawImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, RAWIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", RAWIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, RAWIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool rawImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, RAWIMFILE_TABLE_NAME);
}

bool rawImfileInsert(psDB * dbh, const char *exp_id, const char *class_id, const char *uri, const char *stats)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, RAWIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool rawImfilePop(psDB *dbh, char **exp_id, char **class_id, char **uri, char **stats)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, RAWIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWIMFILE_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, RAWIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, RAWIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, RAWIMFILE_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool rawImfileInsertObject(psDB *dbh, rawImfileRow *object)
{
    return rawImfileInsert(dbh, object->exp_id, object->class_id, object->uri, object->stats);
}

rawImfileRow *rawImfilePopObject(psDB *dbh)
{
    char            exp_id[256];
    char            class_id[256];
    char            uri[256];
    char            stats[256];

    if (!rawImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return rawImfileRowAlloc(exp_id, class_id, uri, stats);
}

bool rawImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  RAWIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, RAWIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", RAWIMFILE_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, RAWIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool rawImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!rawImfileSelectRowsFits(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",
                RAWIMFILE_TABLE_NAME, RAWIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool rawImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, RAWIMFILE_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, RAWIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, RAWIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *rawImfileMetadataFromObject(const rawImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }

    return md;
}

rawImfileRow *rawImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *class_id;
    char            *uri;
    char            *stats;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }

    return rawImfileRowAlloc(exp_id, class_id, uri, stats);
}
psArray *rawImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, RAWIMFILE_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, RAWIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWIMFILE_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++) {
        psArrayAdd(returnSet, 0, rawImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long rawImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        rawImfileRow *object = objects->data[i];
        psMetadata *where = rawImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, RAWIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from rawImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void p1PendingExpRowFree(p1PendingExpRow *object);

p1PendingExpRow *p1PendingExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version)
{
    p1PendingExpRow *object;

    object = psAlloc(sizeof(p1PendingExpRow));
    psMemSetDeallocator(object, (psFreeFunc)p1PendingExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->p1_version = p1_version;

    return object;
}

static void p1PendingExpRowFree(p1PendingExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
    psFree(object->recipe);
}

bool p1PendingExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, P1PENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P1PENDINGEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, P1PENDINGEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool p1PendingExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, P1PENDINGEXP_TABLE_NAME);
}

bool p1PendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, P1PENDINGEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool p1PendingExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p1_version)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, P1PENDINGEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P1PENDINGEXP_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, P1PENDINGEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P1PENDINGEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, P1PENDINGEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, P1PENDINGEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool p1PendingExpInsertObject(psDB *dbh, p1PendingExpRow *object)
{
    return p1PendingExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p1_version);
}

p1PendingExpRow *p1PendingExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];
    char            recipe[256];
    psS32           p1_version;

    if (!p1PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return p1PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version);
}

bool p1PendingExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  P1PENDINGEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, P1PENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P1PENDINGEXP_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, P1PENDINGEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool p1PendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!p1PendingExpSelectRowsFits(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",
                P1PENDINGEXP_TABLE_NAME, P1PENDINGEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool p1PendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P1PENDINGEXP_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, P1PENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P1PENDINGEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, P1PENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *p1PendingExpMetadataFromObject(const p1PendingExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        psFree(md);
        return NULL;
    }

    return md;
}

p1PendingExpRow *p1PendingExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;
    char            *recipe;
    psS32           p1_version;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }
    p1_version = psMetadataLookupS32(&status, md, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        return false;
    }

    return p1PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version);
}
psArray *p1PendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P1PENDINGEXP_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, P1PENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P1PENDINGEXP_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++) {
        psArrayAdd(returnSet, 0, p1PendingExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long p1PendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        p1PendingExpRow *object = objects->data[i];
        psMetadata *where = p1PendingExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, P1PENDINGEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p1PendingExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void p2PendingExpRowFree(p2PendingExpRow *object);

p2PendingExpRow *p2PendingExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    p2PendingExpRow *object;

    object = psAlloc(sizeof(p2PendingExpRow));
    psMemSetDeallocator(object, (psFreeFunc)p2PendingExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->p1_version = p1_version;
    object->p2_version = p2_version;

    return object;
}

static void p2PendingExpRowFree(p2PendingExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
    psFree(object->recipe);
}

bool p2PendingExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, P2PENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2PENDINGEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBCreateTable(dbh, P2PENDINGEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool p2PendingExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, P2PENDINGEXP_TABLE_NAME);
}

bool p2PendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBInsertOneRow(dbh, P2PENDINGEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool p2PendingExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, P2PENDINGEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGEXP_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, P2PENDINGEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, P2PENDINGEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, P2PENDINGEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    psFree(row);

    return true;
}

bool p2PendingExpInsertObject(psDB *dbh, p2PendingExpRow *object)
{
    return p2PendingExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p1_version, object->p2_version);
}

p2PendingExpRow *p2PendingExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];
    char            recipe[256];
    psS32           p1_version;
    psS32           p2_version;

    if (!p2PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return p2PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
}

bool p2PendingExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  P2PENDINGEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, P2PENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2PENDINGEXP_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, P2PENDINGEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool p2PendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!p2PendingExpSelectRowsFits(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",
                P2PENDINGEXP_TABLE_NAME, P2PENDINGEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool p2PendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2PENDINGEXP_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, P2PENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, P2PENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *p2PendingExpMetadataFromObject(const p2PendingExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    return md;
}

p2PendingExpRow *p2PendingExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;
    char            *recipe;
    psS32           p1_version;
    psS32           p2_version;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }
    p1_version = psMetadataLookupS32(&status, md, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    return p2PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
}
psArray *p2PendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2PENDINGEXP_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, P2PENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGEXP_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++) {
        psArrayAdd(returnSet, 0, p2PendingExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long p2PendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        p2PendingExpRow *object = objects->data[i];
        psMetadata *where = p2PendingExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, P2PENDINGEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2PendingExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void p2PendingImfileRowFree(p2PendingImfileRow *object);

p2PendingImfileRow *p2PendingImfileRowAlloc(const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    p2PendingImfileRow *object;

    object = psAlloc(sizeof(p2PendingImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)p2PendingImfileRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->p1_version = p1_version;
    object->p2_version = p2_version;

    return object;
}

static void p2PendingImfileRowFree(p2PendingImfileRow *object)
{
    psFree(object->exp_id);
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
    psFree(object->recipe);
}

bool p2PendingImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, P2PENDINGIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2PENDINGIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBCreateTable(dbh, P2PENDINGIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool p2PendingImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, P2PENDINGIMFILE_TABLE_NAME);
}

bool p2PendingImfileInsert(psDB * dbh, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBInsertOneRow(dbh, P2PENDINGIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool p2PendingImfilePop(psDB *dbh, char **exp_id, char **class_id, char **uri, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, P2PENDINGIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGIMFILE_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, P2PENDINGIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, P2PENDINGIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, P2PENDINGIMFILE_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    psFree(row);

    return true;
}

bool p2PendingImfileInsertObject(psDB *dbh, p2PendingImfileRow *object)
{
    return p2PendingImfileInsert(dbh, object->exp_id, object->class_id, object->uri, object->stats, object->recipe, object->p1_version, object->p2_version);
}

p2PendingImfileRow *p2PendingImfilePopObject(psDB *dbh)
{
    char            exp_id[256];
    char            class_id[256];
    char            uri[256];
    char            stats[256];
    char            recipe[256];
    psS32           p1_version;
    psS32           p2_version;

    if (!p2PendingImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return p2PendingImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
}

bool p2PendingImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  P2PENDINGIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, P2PENDINGIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2PENDINGIMFILE_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, P2PENDINGIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool p2PendingImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!p2PendingImfileSelectRowsFits(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",
                P2PENDINGIMFILE_TABLE_NAME, P2PENDINGIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool p2PendingImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2PENDINGIMFILE_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, P2PENDINGIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, P2PENDINGIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *p2PendingImfileMetadataFromObject(const p2PendingImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    return md;
}

p2PendingImfileRow *p2PendingImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *class_id;
    char            *uri;
    char            *stats;
    char            *recipe;
    psS32           p1_version;
    psS32           p2_version;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }
    p1_version = psMetadataLookupS32(&status, md, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    return p2PendingImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
}
psArray *p2PendingImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2PENDINGIMFILE_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, P2PENDINGIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGIMFILE_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++) {
        psArrayAdd(returnSet, 0, p2PendingImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long p2PendingImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        p2PendingImfileRow *object = objects->data[i];
        psMetadata *where = p2PendingImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, P2PENDINGIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2PendingImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void p2DoneExpRowFree(p2DoneExpRow *object);

p2DoneExpRow *p2DoneExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    p2DoneExpRow    *object;

    object = psAlloc(sizeof(p2DoneExpRow));
    psMemSetDeallocator(object, (psFreeFunc)p2DoneExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->p1_version = p1_version;
    object->p2_version = p2_version;

    return object;
}

static void p2DoneExpRowFree(p2DoneExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
    psFree(object->recipe);
}

bool p2DoneExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, P2DONEEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2DONEEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBCreateTable(dbh, P2DONEEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool p2DoneExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, P2DONEEXP_TABLE_NAME);
}

bool p2DoneExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBInsertOneRow(dbh, P2DONEEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool p2DoneExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, P2DONEEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEEXP_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, P2DONEEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, P2DONEEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, P2DONEEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    psFree(row);

    return true;
}

bool p2DoneExpInsertObject(psDB *dbh, p2DoneExpRow *object)
{
    return p2DoneExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p1_version, object->p2_version);
}

p2DoneExpRow *p2DoneExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];
    char            recipe[256];
    psS32           p1_version;
    psS32           p2_version;

    if (!p2DoneExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return p2DoneExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
}

bool p2DoneExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  P2DONEEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, P2DONEEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2DONEEXP_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, P2DONEEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool p2DoneExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!p2DoneExpSelectRowsFits(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",
                P2DONEEXP_TABLE_NAME, P2DONEEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool p2DoneExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2DONEEXP_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, P2DONEEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, P2DONEEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *p2DoneExpMetadataFromObject(const p2DoneExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    return md;
}

p2DoneExpRow *p2DoneExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;
    char            *recipe;
    psS32           p1_version;
    psS32           p2_version;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }
    p1_version = psMetadataLookupS32(&status, md, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    return p2DoneExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
}
psArray *p2DoneExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2DONEEXP_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, P2DONEEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEEXP_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++) {
        psArrayAdd(returnSet, 0, p2DoneExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long p2DoneExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        p2DoneExpRow *object = objects->data[i];
        psMetadata *where = p2DoneExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, P2DONEEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2DoneExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void p2DoneImfileRowFree(p2DoneImfileRow *object);

p2DoneImfileRow *p2DoneImfileRowAlloc(const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    p2DoneImfileRow *object;

    object = psAlloc(sizeof(p2DoneImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)p2DoneImfileRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->p1_version = p1_version;
    object->p2_version = p2_version;

    return object;
}

static void p2DoneImfileRowFree(p2DoneImfileRow *object)
{
    psFree(object->exp_id);
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
    psFree(object->recipe);
}

bool p2DoneImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, P2DONEIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2DONEIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBCreateTable(dbh, P2DONEIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool p2DoneImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, P2DONEIMFILE_TABLE_NAME);
}

bool p2DoneImfileInsert(psDB * dbh, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    status = psDBInsertOneRow(dbh, P2DONEIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool p2DoneImfilePop(psDB *dbh, char **exp_id, char **class_id, char **uri, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, P2DONEIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEIMFILE_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, P2DONEIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, P2DONEIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, P2DONEIMFILE_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    psFree(row);

    return true;
}

bool p2DoneImfileInsertObject(psDB *dbh, p2DoneImfileRow *object)
{
    return p2DoneImfileInsert(dbh, object->exp_id, object->class_id, object->uri, object->stats, object->recipe, object->p1_version, object->p2_version);
}

p2DoneImfileRow *p2DoneImfilePopObject(psDB *dbh)
{
    char            exp_id[256];
    char            class_id[256];
    char            uri[256];
    char            stats[256];
    char            recipe[256];
    psS32           p1_version;
    psS32           p2_version;

    if (!p2DoneImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return p2DoneImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
}

bool p2DoneImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  P2DONEIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, P2DONEIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2DONEIMFILE_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, P2DONEIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool p2DoneImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!p2DoneImfileSelectRowsFits(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",
                P2DONEIMFILE_TABLE_NAME, P2DONEIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool p2DoneImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2DONEIMFILE_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, P2DONEIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, P2DONEIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *p2DoneImfileMetadataFromObject(const p2DoneImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
        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;
    }

    return md;
}

p2DoneImfileRow *p2DoneImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *class_id;
    char            *uri;
    char            *stats;
    char            *recipe;
    psS32           p1_version;
    psS32           p2_version;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }
    p1_version = psMetadataLookupS32(&status, md, "p1_version");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
        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;
    }

    return p2DoneImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
}
psArray *p2DoneImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P2DONEIMFILE_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, P2DONEIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEIMFILE_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++) {
        psArrayAdd(returnSet, 0, p2DoneImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long p2DoneImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        p2DoneImfileRow *object = objects->data[i];
        psMetadata *where = p2DoneImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, P2DONEIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2DoneImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void p3PendingExpRowFree(p3PendingExpRow *object);

p3PendingExpRow *p3PendingExpRowAlloc(const char *exp_id, const char *camera, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p2_version, psS32 p3_version)
{
    p3PendingExpRow *object;

    object = psAlloc(sizeof(p3PendingExpRow));
    psMemSetDeallocator(object, (psFreeFunc)p3PendingExpRowFree);

    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->p2_version = p2_version;
    object->p3_version = p3_version;

    return object;
}

static void p3PendingExpRowFree(p3PendingExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
    psFree(object->recipe);
}

bool p3PendingExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, P3PENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P3PENDINGEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!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, P3PENDINGEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool p3PendingExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, P3PENDINGEXP_TABLE_NAME);
}

bool p3PendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p2_version, psS32 p3_version)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        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, P3PENDINGEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool p3PendingExpPop(psDB *dbh, char **exp_id, char **camera, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p2_version, psS32 *p3_version)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, P3PENDINGEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P3PENDINGEXP_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, P3PENDINGEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P3PENDINGEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, P3PENDINGEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, P3PENDINGEXP_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_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *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 p3PendingExpInsertObject(psDB *dbh, p3PendingExpRow *object)
{
    return p3PendingExpInsert(dbh, object->exp_id, object->camera, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p2_version, object->p3_version);
}

p3PendingExpRow *p3PendingExpPopObject(psDB *dbh)
{
    char            exp_id[256];
    char            camera[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];
    char            recipe[256];
    psS32           p2_version;
    psS32           p3_version;

    if (!p3PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p2_version, &p3_version)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return p3PendingExpRowAlloc(exp_id, camera, exp_type, imfiles, filter, stats, recipe, p2_version, p3_version);
}

bool p3PendingExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  P3PENDINGEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, P3PENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P3PENDINGEXP_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, P3PENDINGEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool p3PendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!p3PendingExpSelectRowsFits(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",
                P3PENDINGEXP_TABLE_NAME, P3PENDINGEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool p3PendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P3PENDINGEXP_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, P3PENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P3PENDINGEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, P3PENDINGEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *p3PendingExpMetadataFromObject(const p3PendingExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        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;
}

p3PendingExpRow *p3PendingExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *exp_id;
    char            *camera;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;
    char            *recipe;
    psS32           p2_version;
    psS32           p3_version;

    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        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 p3PendingExpRowAlloc(exp_id, camera, exp_type, imfiles, filter, stats, recipe, p2_version, p3_version);
}
psArray *p3PendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, P3PENDINGEXP_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, P3PENDINGEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P3PENDINGEXP_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++) {
        psArrayAdd(returnSet, 0, p3PendingExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long p3PendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        p3PendingExpRow *object = objects->data[i];
        psMetadata *where = p3PendingExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, P3PENDINGEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p3PendingExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detRunRowFree(detRunRow *object);

detRunRow *detRunRowAlloc(const char *det_type)
{
    detRunRow       *object;

    object = psAlloc(sizeof(detRunRow));
    psMemSetDeallocator(object, (psFreeFunc)detRunRowFree);

    object->det_type = psStringCopy(det_type);

    return object;
}

static void detRunRowFree(detRunRow *object)
{
    psFree(object->det_type);
}

bool detRunCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETRUN_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETRUN_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, "Key", "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_type");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, DETRUN_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detRunDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETRUN_TABLE_NAME);
}

bool detRunInsert(psDB * dbh, const char *det_type)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, NULL, psStringCopy(det_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_type");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETRUN_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detRunPop(psDB *dbh, char **det_type)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETRUN_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRUN_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, DETRUN_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRUN_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETRUN_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETRUN_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);

    *det_type = psMetadataLookupPtr(&status, row, "det_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_type");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool detRunInsertObject(psDB *dbh, detRunRow *object)
{
    return detRunInsert(dbh, object->det_type);
}

detRunRow *detRunPopObject(psDB *dbh)
{
    char            det_type[256];

    if (!detRunPop(dbh, (char **)&det_type)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detRunRowAlloc(det_type);
}

bool detRunInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETRUN_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETRUN_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETRUN_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, DETRUN_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detRunPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detRunSelectRowsFits(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",
                DETRUN_TABLE_NAME, DETRUN_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detRunSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETRUN_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, DETRUN_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRUN_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETRUN_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detRunMetadataFromObject(const detRunRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, NULL, psStringCopy(object->det_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_type");
        psFree(md);
        return NULL;
    }

    return md;
}

detRunRow *detRunObjectFromMetadata(psMetadata *md)
{
    bool            status;
    char            *det_type;

    det_type = psMetadataLookupPtr(&status, md, "det_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_type");
        return false;
    }

    return detRunRowAlloc(det_type);
}
psArray *detRunSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETRUN_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, DETRUN_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRUN_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++) {
        psArrayAdd(returnSet, 0, detRunObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detRunDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detRunRow *object = objects->data[i];
        psMetadata *where = detRunMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETRUN_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detRun");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detInputExpRowFree(detInputExpRow *object);

detInputExpRow *detInputExpRowAlloc(psS32 det_id, psS32 iteration, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
{
    detInputExpRow  *object;

    object = psAlloc(sizeof(detInputExpRow));
    psMemSetDeallocator(object, (psFreeFunc)detInputExpRowFree);

    object->det_id = det_id;
    object->iteration = iteration;
    object->exp_id = psStringCopy(exp_id);
    object->camera = psStringCopy(camera);
    object->telescope = psStringCopy(telescope);
    object->exp_type = psStringCopy(exp_type);
    object->imfiles = imfiles;
    object->filter = psStringCopy(filter);
    object->stats = psStringCopy(stats);

    return object;
}

static void detInputExpRowFree(detInputExpRow *object)
{
    psFree(object->exp_id);
    psFree(object->camera);
    psFree(object->telescope);
    psFree(object->exp_type);
    psFree(object->filter);
    psFree(object->stats);
}

bool detInputExpCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETINPUTEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETINPUTEXP_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, DETINPUTEXP_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detInputExpDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETINPUTEXP_TABLE_NAME);
}

bool detInputExpInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETINPUTEXP_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detInputExpPop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETINPUTEXP_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETINPUTEXP_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, DETINPUTEXP_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETINPUTEXP_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETINPUTEXP_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETINPUTEXP_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *iteration = psMetadataLookupS32(&status, row, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        psFree(row);
        return false;
    }
    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *camera = psMetadataLookupPtr(&status, row, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        psFree(row);
        return false;
    }
    *telescope = psMetadataLookupPtr(&status, row, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        psFree(row);
        return false;
    }
    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        psFree(row);
        return false;
    }
    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        psFree(row);
        return false;
    }
    *filter = psMetadataLookupPtr(&status, row, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool detInputExpInsertObject(psDB *dbh, detInputExpRow *object)
{
    return detInputExpInsert(dbh, object->det_id, object->iteration, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats);
}

detInputExpRow *detInputExpPopObject(psDB *dbh)
{
    psS32           det_id;
    psS32           iteration;
    char            exp_id[256];
    char            camera[256];
    char            telescope[256];
    char            exp_type[256];
    psS32           imfiles;
    char            filter[256];
    char            stats[256];

    if (!detInputExpPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detInputExpRowAlloc(det_id, iteration, exp_id, camera, telescope, exp_type, imfiles, filter, stats);
}

bool detInputExpInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETINPUTEXP_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETINPUTEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETINPUTEXP_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, DETINPUTEXP_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detInputExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detInputExpSelectRowsFits(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",
                DETINPUTEXP_TABLE_NAME, DETINPUTEXP_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detInputExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETINPUTEXP_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, DETINPUTEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETINPUTEXP_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETINPUTEXP_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detInputExpMetadataFromObject(const detInputExpRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }

    return md;
}

detInputExpRow *detInputExpObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    psS32           iteration;
    char            *exp_id;
    char            *camera;
    char            *telescope;
    char            *exp_type;
    psS32           imfiles;
    char            *filter;
    char            *stats;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    iteration = psMetadataLookupS32(&status, md, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        return false;
    }
    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    camera = psMetadataLookupPtr(&status, md, "camera");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
        return false;
    }
    telescope = psMetadataLookupPtr(&status, md, "telescope");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
        return false;
    }
    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
        return false;
    }
    imfiles = psMetadataLookupS32(&status, md, "imfiles");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
        return false;
    }
    filter = psMetadataLookupPtr(&status, md, "filter");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }

    return detInputExpRowAlloc(det_id, iteration, exp_id, camera, telescope, exp_type, imfiles, filter, stats);
}
psArray *detInputExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETINPUTEXP_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, DETINPUTEXP_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETINPUTEXP_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++) {
        psArrayAdd(returnSet, 0, detInputExpObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detInputExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detInputExpRow *object = objects->data[i];
        psMetadata *where = detInputExpMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETINPUTEXP_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detInputExp");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detProcessedImfileRowFree(detProcessedImfileRow *object);

detProcessedImfileRow *detProcessedImfileRowAlloc(psS32 det_id, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    detProcessedImfileRow *object;

    object = psAlloc(sizeof(detProcessedImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)detProcessedImfileRowFree);

    object->det_id = det_id;
    object->exp_id = psStringCopy(exp_id);
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);

    return object;
}

static void detProcessedImfileRowFree(detProcessedImfileRow *object)
{
    psFree(object->exp_id);
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
    psFree(object->recipe);
}

bool detProcessedImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETPROCESSEDIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETPROCESSEDIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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;
    }

    status = psDBCreateTable(dbh, DETPROCESSEDIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detProcessedImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETPROCESSEDIMFILE_TABLE_NAME);
}

bool detProcessedImfileInsert(psDB * dbh, psS32 det_id, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETPROCESSEDIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detProcessedImfilePop(psDB *dbh, psS32 *det_id, char **exp_id, char **class_id, char **uri, char **stats, char **recipe)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETPROCESSEDIMFILE_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, DETPROCESSEDIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETPROCESSEDIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETPROCESSEDIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETPROCESSEDIMFILE_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }

    psFree(row);

    return true;
}

bool detProcessedImfileInsertObject(psDB *dbh, detProcessedImfileRow *object)
{
    return detProcessedImfileInsert(dbh, object->det_id, object->exp_id, object->class_id, object->uri, object->stats, object->recipe);
}

detProcessedImfileRow *detProcessedImfilePopObject(psDB *dbh)
{
    psS32           det_id;
    char            exp_id[256];
    char            class_id[256];
    char            uri[256];
    char            stats[256];
    char            recipe[256];

    if (!detProcessedImfilePop(dbh, &det_id, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detProcessedImfileRowAlloc(det_id, exp_id, class_id, uri, stats, recipe);
}

bool detProcessedImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETPROCESSEDIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETPROCESSEDIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETPROCESSEDIMFILE_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, DETPROCESSEDIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detProcessedImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detProcessedImfileSelectRowsFits(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",
                DETPROCESSEDIMFILE_TABLE_NAME, DETPROCESSEDIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detProcessedImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETPROCESSEDIMFILE_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, DETPROCESSEDIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETPROCESSEDIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETPROCESSEDIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detProcessedImfileMetadataFromObject(const detProcessedImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }

    return md;
}

detProcessedImfileRow *detProcessedImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    char            *exp_id;
    char            *class_id;
    char            *uri;
    char            *stats;
    char            *recipe;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }

    return detProcessedImfileRowAlloc(det_id, exp_id, class_id, uri, stats, recipe);
}
psArray *detProcessedImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETPROCESSEDIMFILE_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, DETPROCESSEDIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETPROCESSEDIMFILE_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++) {
        psArrayAdd(returnSet, 0, detProcessedImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detProcessedImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detProcessedImfileRow *object = objects->data[i];
        psMetadata *where = detProcessedImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detProcessedImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detStackedImfileRowFree(detStackedImfileRow *object);

detStackedImfileRow *detStackedImfileRowAlloc(psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    detStackedImfileRow *object;

    object = psAlloc(sizeof(detStackedImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)detStackedImfileRowFree);

    object->det_id = det_id;
    object->iteration = iteration;
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);

    return object;
}

static void detStackedImfileRowFree(detStackedImfileRow *object)
{
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
    psFree(object->recipe);
}

bool detStackedImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETSTACKEDIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETSTACKEDIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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;
    }

    status = psDBCreateTable(dbh, DETSTACKEDIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detStackedImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETSTACKEDIMFILE_TABLE_NAME);
}

bool detStackedImfileInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETSTACKEDIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detStackedImfilePop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **class_id, char **uri, char **stats, char **recipe)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETSTACKEDIMFILE_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, DETSTACKEDIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETSTACKEDIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETSTACKEDIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETSTACKEDIMFILE_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *iteration = psMetadataLookupS32(&status, row, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }

    psFree(row);

    return true;
}

bool detStackedImfileInsertObject(psDB *dbh, detStackedImfileRow *object)
{
    return detStackedImfileInsert(dbh, object->det_id, object->iteration, object->class_id, object->uri, object->stats, object->recipe);
}

detStackedImfileRow *detStackedImfilePopObject(psDB *dbh)
{
    psS32           det_id;
    psS32           iteration;
    char            class_id[256];
    char            uri[256];
    char            stats[256];
    char            recipe[256];

    if (!detStackedImfilePop(dbh, &det_id, &iteration, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detStackedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
}

bool detStackedImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETSTACKEDIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETSTACKEDIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETSTACKEDIMFILE_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, DETSTACKEDIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detStackedImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detStackedImfileSelectRowsFits(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",
                DETSTACKEDIMFILE_TABLE_NAME, DETSTACKEDIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detStackedImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETSTACKEDIMFILE_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, DETSTACKEDIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETSTACKEDIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETSTACKEDIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detStackedImfileMetadataFromObject(const detStackedImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }

    return md;
}

detStackedImfileRow *detStackedImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    psS32           iteration;
    char            *class_id;
    char            *uri;
    char            *stats;
    char            *recipe;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    iteration = psMetadataLookupS32(&status, md, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }

    return detStackedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
}
psArray *detStackedImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETSTACKEDIMFILE_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, DETSTACKEDIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETSTACKEDIMFILE_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++) {
        psArrayAdd(returnSet, 0, detStackedImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detStackedImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detStackedImfileRow *object = objects->data[i];
        psMetadata *where = detStackedImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detStackedImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detNormalizedImfileRowFree(detNormalizedImfileRow *object);

detNormalizedImfileRow *detNormalizedImfileRowAlloc(psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    detNormalizedImfileRow *object;

    object = psAlloc(sizeof(detNormalizedImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)detNormalizedImfileRowFree);

    object->det_id = det_id;
    object->iteration = iteration;
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);

    return object;
}

static void detNormalizedImfileRowFree(detNormalizedImfileRow *object)
{
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
    psFree(object->recipe);
}

bool detNormalizedImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETNORMALIZEDIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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;
    }

    status = psDBCreateTable(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detNormalizedImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETNORMALIZEDIMFILE_TABLE_NAME);
}

bool detNormalizedImfileInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detNormalizedImfilePop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **class_id, char **uri, char **stats, char **recipe)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETNORMALIZEDIMFILE_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, DETNORMALIZEDIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETNORMALIZEDIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETNORMALIZEDIMFILE_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *iteration = psMetadataLookupS32(&status, row, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }

    psFree(row);

    return true;
}

bool detNormalizedImfileInsertObject(psDB *dbh, detNormalizedImfileRow *object)
{
    return detNormalizedImfileInsert(dbh, object->det_id, object->iteration, object->class_id, object->uri, object->stats, object->recipe);
}

detNormalizedImfileRow *detNormalizedImfilePopObject(psDB *dbh)
{
    psS32           det_id;
    psS32           iteration;
    char            class_id[256];
    char            uri[256];
    char            stats[256];
    char            recipe[256];

    if (!detNormalizedImfilePop(dbh, &det_id, &iteration, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detNormalizedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
}

bool detNormalizedImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETNORMALIZEDIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETNORMALIZEDIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETNORMALIZEDIMFILE_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, DETNORMALIZEDIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detNormalizedImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detNormalizedImfileSelectRowsFits(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",
                DETNORMALIZEDIMFILE_TABLE_NAME, DETNORMALIZEDIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detNormalizedImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETNORMALIZEDIMFILE_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, DETNORMALIZEDIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETNORMALIZEDIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detNormalizedImfileMetadataFromObject(const detNormalizedImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }

    return md;
}

detNormalizedImfileRow *detNormalizedImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    psS32           iteration;
    char            *class_id;
    char            *uri;
    char            *stats;
    char            *recipe;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    iteration = psMetadataLookupS32(&status, md, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }

    return detNormalizedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
}
psArray *detNormalizedImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETNORMALIZEDIMFILE_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, DETNORMALIZEDIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETNORMALIZEDIMFILE_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++) {
        psArrayAdd(returnSet, 0, detNormalizedImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detNormalizedImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detNormalizedImfileRow *object = objects->data[i];
        psMetadata *where = detNormalizedImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detNormalizedImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detMasterFrameRowFree(detMasterFrameRow *object);

detMasterFrameRow *detMasterFrameRowAlloc(psS32 det_id, psS32 iteration, const char *comment)
{
    detMasterFrameRow *object;

    object = psAlloc(sizeof(detMasterFrameRow));
    psMemSetDeallocator(object, (psFreeFunc)detMasterFrameRowFree);

    object->det_id = det_id;
    object->iteration = iteration;
    object->comment = psStringCopy(comment);

    return object;
}

static void detMasterFrameRowFree(detMasterFrameRow *object)
{
    psFree(object->comment);
}

bool detMasterFrameCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETMASTERFRAME_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETMASTERFRAME_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item comment");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, DETMASTERFRAME_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detMasterFrameDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETMASTERFRAME_TABLE_NAME);
}

bool detMasterFrameInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *comment)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, psStringCopy(comment))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item comment");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETMASTERFRAME_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detMasterFramePop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **comment)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETMASTERFRAME_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERFRAME_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, DETMASTERFRAME_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERFRAME_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETMASTERFRAME_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETMASTERFRAME_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *iteration = psMetadataLookupS32(&status, row, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        psFree(row);
        return false;
    }
    *comment = psMetadataLookupPtr(&status, row, "comment");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item comment");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool detMasterFrameInsertObject(psDB *dbh, detMasterFrameRow *object)
{
    return detMasterFrameInsert(dbh, object->det_id, object->iteration, object->comment);
}

detMasterFrameRow *detMasterFramePopObject(psDB *dbh)
{
    psS32           det_id;
    psS32           iteration;
    char            comment[256];

    if (!detMasterFramePop(dbh, &det_id, &iteration, (char **)&comment)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detMasterFrameRowAlloc(det_id, iteration, comment);
}

bool detMasterFrameInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETMASTERFRAME_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETMASTERFRAME_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETMASTERFRAME_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, DETMASTERFRAME_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detMasterFramePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detMasterFrameSelectRowsFits(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",
                DETMASTERFRAME_TABLE_NAME, DETMASTERFRAME_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detMasterFrameSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETMASTERFRAME_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, DETMASTERFRAME_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERFRAME_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETMASTERFRAME_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detMasterFrameMetadataFromObject(const detMasterFrameRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, psStringCopy(object->comment))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item comment");
        psFree(md);
        return NULL;
    }

    return md;
}

detMasterFrameRow *detMasterFrameObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    psS32           iteration;
    char            *comment;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    iteration = psMetadataLookupS32(&status, md, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        return false;
    }
    comment = psMetadataLookupPtr(&status, md, "comment");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item comment");
        return false;
    }

    return detMasterFrameRowAlloc(det_id, iteration, comment);
}
psArray *detMasterFrameSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETMASTERFRAME_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, DETMASTERFRAME_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERFRAME_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++) {
        psArrayAdd(returnSet, 0, detMasterFrameObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detMasterFrameDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detMasterFrameRow *object = objects->data[i];
        psMetadata *where = detMasterFrameMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETMASTERFRAME_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detMasterFrame");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detMasterImfileRowFree(detMasterImfileRow *object);

detMasterImfileRow *detMasterImfileRowAlloc(psS32 det_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    detMasterImfileRow *object;

    object = psAlloc(sizeof(detMasterImfileRow));
    psMemSetDeallocator(object, (psFreeFunc)detMasterImfileRowFree);

    object->det_id = det_id;
    object->class_id = psStringCopy(class_id);
    object->uri = psStringCopy(uri);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);

    return object;
}

static void detMasterImfileRowFree(detMasterImfileRow *object)
{
    psFree(object->class_id);
    psFree(object->uri);
    psFree(object->stats);
    psFree(object->recipe);
}

bool detMasterImfileCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETMASTERIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETMASTERIMFILE_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        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, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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;
    }

    status = psDBCreateTable(dbh, DETMASTERIMFILE_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detMasterImfileDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETMASTERIMFILE_TABLE_NAME);
}

bool detMasterImfileInsert(psDB * dbh, psS32 det_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETMASTERIMFILE_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detMasterImfilePop(psDB *dbh, psS32 *det_id, char **class_id, char **uri, char **stats, char **recipe)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETMASTERIMFILE_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERIMFILE_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, DETMASTERIMFILE_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERIMFILE_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETMASTERIMFILE_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETMASTERIMFILE_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        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;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }

    psFree(row);

    return true;
}

bool detMasterImfileInsertObject(psDB *dbh, detMasterImfileRow *object)
{
    return detMasterImfileInsert(dbh, object->det_id, object->class_id, object->uri, object->stats, object->recipe);
}

detMasterImfileRow *detMasterImfilePopObject(psDB *dbh)
{
    psS32           det_id;
    char            class_id[256];
    char            uri[256];
    char            stats[256];
    char            recipe[256];

    if (!detMasterImfilePop(dbh, &det_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detMasterImfileRowAlloc(det_id, class_id, uri, stats, recipe);
}

bool detMasterImfileInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETMASTERIMFILE_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETMASTERIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETMASTERIMFILE_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, DETMASTERIMFILE_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detMasterImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detMasterImfileSelectRowsFits(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",
                DETMASTERIMFILE_TABLE_NAME, DETMASTERIMFILE_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detMasterImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETMASTERIMFILE_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, DETMASTERIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERIMFILE_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETMASTERIMFILE_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detMasterImfileMetadataFromObject(const detMasterImfileRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }

    return md;
}

detMasterImfileRow *detMasterImfileObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    char            *class_id;
    char            *uri;
    char            *stats;
    char            *recipe;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    uri = psMetadataLookupPtr(&status, md, "uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }

    return detMasterImfileRowAlloc(det_id, class_id, uri, stats, recipe);
}
psArray *detMasterImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETMASTERIMFILE_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, DETMASTERIMFILE_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERIMFILE_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++) {
        psArrayAdd(returnSet, 0, detMasterImfileObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detMasterImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detMasterImfileRow *object = objects->data[i];
        psMetadata *where = detMasterImfileMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETMASTERIMFILE_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detMasterImfile");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detResidImfileAnalysisRowFree(detResidImfileAnalysisRow *object);

detResidImfileAnalysisRow *detResidImfileAnalysisRowAlloc(psS32 det_id, psS32 iteration, const char *exp_id, const char *class_id, const char *stats, const char *recipe)
{
    detResidImfileAnalysisRow *object;

    object = psAlloc(sizeof(detResidImfileAnalysisRow));
    psMemSetDeallocator(object, (psFreeFunc)detResidImfileAnalysisRowFree);

    object->det_id = det_id;
    object->iteration = iteration;
    object->exp_id = psStringCopy(exp_id);
    object->class_id = psStringCopy(class_id);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);

    return object;
}

static void detResidImfileAnalysisRowFree(detResidImfileAnalysisRow *object)
{
    psFree(object->exp_id);
    psFree(object->class_id);
    psFree(object->stats);
    psFree(object->recipe);
}

bool detResidImfileAnalysisCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETRESIDIMFILEANALYSIS_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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;
    }

    status = psDBCreateTable(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detResidImfileAnalysisDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME);
}

bool detResidImfileAnalysisInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *exp_id, const char *class_id, const char *stats, const char *recipe)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detResidImfileAnalysisPop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **exp_id, char **class_id, char **stats, char **recipe)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDIMFILEANALYSIS_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, DETRESIDIMFILEANALYSIS_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETRESIDIMFILEANALYSIS_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETRESIDIMFILEANALYSIS_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *iteration = psMetadataLookupS32(&status, row, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        psFree(row);
        return false;
    }
    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *class_id = psMetadataLookupPtr(&status, row, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }

    psFree(row);

    return true;
}

bool detResidImfileAnalysisInsertObject(psDB *dbh, detResidImfileAnalysisRow *object)
{
    return detResidImfileAnalysisInsert(dbh, object->det_id, object->iteration, object->exp_id, object->class_id, object->stats, object->recipe);
}

detResidImfileAnalysisRow *detResidImfileAnalysisPopObject(psDB *dbh)
{
    psS32           det_id;
    psS32           iteration;
    char            exp_id[256];
    char            class_id[256];
    char            stats[256];
    char            recipe[256];

    if (!detResidImfileAnalysisPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&class_id, (char **)&stats, (char **)&recipe)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detResidImfileAnalysisRowAlloc(det_id, iteration, exp_id, class_id, stats, recipe);
}

bool detResidImfileAnalysisInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETRESIDIMFILEANALYSIS_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETRESIDIMFILEANALYSIS_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETRESIDIMFILEANALYSIS_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, DETRESIDIMFILEANALYSIS_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detResidImfileAnalysisPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detResidImfileAnalysisSelectRowsFits(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",
                DETRESIDIMFILEANALYSIS_TABLE_NAME, DETRESIDIMFILEANALYSIS_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detResidImfileAnalysisSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETRESIDIMFILEANALYSIS_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, DETRESIDIMFILEANALYSIS_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETRESIDIMFILEANALYSIS_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detResidImfileAnalysisMetadataFromObject(const detResidImfileAnalysisRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }

    return md;
}

detResidImfileAnalysisRow *detResidImfileAnalysisObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    psS32           iteration;
    char            *exp_id;
    char            *class_id;
    char            *stats;
    char            *recipe;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    iteration = psMetadataLookupS32(&status, md, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        return false;
    }
    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    class_id = psMetadataLookupPtr(&status, md, "class_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }

    return detResidImfileAnalysisRowAlloc(det_id, iteration, exp_id, class_id, stats, recipe);
}
psArray *detResidImfileAnalysisSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETRESIDIMFILEANALYSIS_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, DETRESIDIMFILEANALYSIS_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDIMFILEANALYSIS_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++) {
        psArrayAdd(returnSet, 0, detResidImfileAnalysisObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detResidImfileAnalysisDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detResidImfileAnalysisRow *object = objects->data[i];
        psMetadata *where = detResidImfileAnalysisMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detResidImfileAnalysis");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
static void detResidExpAnalysisRowFree(detResidExpAnalysisRow *object);

detResidExpAnalysisRow *detResidExpAnalysisRowAlloc(psS32 det_id, psS32 iteration, const char *exp_id, const char *stats, const char *recipe, bool accept)
{
    detResidExpAnalysisRow *object;

    object = psAlloc(sizeof(detResidExpAnalysisRow));
    psMemSetDeallocator(object, (psFreeFunc)detResidExpAnalysisRowFree);

    object->det_id = det_id;
    object->iteration = iteration;
    object->exp_id = psStringCopy(exp_id);
    object->stats = psStringCopy(stats);
    object->recipe = psStringCopy(recipe);
    object->accept = accept;

    return object;
}

static void detResidExpAnalysisRowFree(detResidExpAnalysisRow *object)
{
    psFree(object->exp_id);
    psFree(object->stats);
    psFree(object->recipe);
}

bool detResidExpAnalysisCreateTable(psDB *dbh)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAdd(md, PS_LIST_TAIL, DETRESIDEXPANALYSIS_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        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 (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, false)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item accept");
        psFree(md);
        return false;
    }

    status = psDBCreateTable(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, md);

    psFree(md);

    return status;
}

bool detResidExpAnalysisDropTable(psDB *dbh)
{
    return psDBDropTable(dbh, DETRESIDEXPANALYSIS_TABLE_NAME);
}

bool detResidExpAnalysisInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *exp_id, const char *stats, const char *recipe, bool accept)
{
    psMetadata      *md;
    bool            status;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return false;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return false;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, accept)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item accept");
        psFree(md);
        return false;
    }

    status = psDBInsertOneRow(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, md);
    psFree(md);

    return status;
}

bool detResidExpAnalysisPop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **exp_id, char **stats, char **recipe, bool *accept)
{
    psArray         *rowSet;
    psMetadata      *row;
    psMetadata      *popped;
    long            deleted;
    bool            status;
    int             rowID;

    rowSet = psDBSelectRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, NULL, 1);
    if (!rowSet) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDEXPANALYSIS_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, DETRESIDEXPANALYSIS_INDEX_NAME);
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
        psFree(row);
        return NULL;
    }

    popped = psMetadataAlloc();
    psMetadataAddS32(popped, PS_LIST_TAIL, DETRESIDEXPANALYSIS_INDEX_NAME, 0, NULL, rowID);

    deleted = psDBDeleteRows(dbh, DETRESIDEXPANALYSIS_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);

    *det_id = psMetadataLookupS32(&status, row, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        psFree(row);
        return false;
    }
    *iteration = psMetadataLookupS32(&status, row, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        psFree(row);
        return false;
    }
    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        psFree(row);
        return false;
    }
    *stats = psMetadataLookupPtr(&status, row, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        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;
    }
    *accept = psMetadataLookupBool(&status, row, "accept");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item accept");
        psFree(row);
        return false;
    }

    psFree(row);

    return true;
}

bool detResidExpAnalysisInsertObject(psDB *dbh, detResidExpAnalysisRow *object)
{
    return detResidExpAnalysisInsert(dbh, object->det_id, object->iteration, object->exp_id, object->stats, object->recipe, object->accept);
}

detResidExpAnalysisRow *detResidExpAnalysisPopObject(psDB *dbh)
{
    psS32           det_id;
    psS32           iteration;
    char            exp_id[256];
    char            stats[256];
    char            recipe[256];
    bool            accept;

    if (!detResidExpAnalysisPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&stats, (char **)&recipe, &accept)) {
        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
        return NULL;
    }

    return detResidExpAnalysisRowAlloc(det_id, iteration, exp_id, stats, recipe, accept);
}

bool detResidExpAnalysisInsertFits(psDB *dbh, const psFits *fits)
{
    psArray         *rowSet;

    // move to (the first?) extension named  DETRESIDEXPANALYSIS_TABLE_NAME
    if (!psFitsMoveExtName(fits, DETRESIDEXPANALYSIS_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETRESIDEXPANALYSIS_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, DETRESIDEXPANALYSIS_TABLE_NAME, rowSet)) {
        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

bool detResidExpAnalysisPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
{
    char            query[MAX_STRING_LENGTH];

    if (!detResidExpAnalysisSelectRowsFits(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",
                DETRESIDEXPANALYSIS_TABLE_NAME, DETRESIDEXPANALYSIS_INDEX_NAME, limit) < 0) {
        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
        return false;
    }
            
    if (!p_psDBRunQuery(dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database query failed");
        return false;
    }

    return true;
}

bool detResidExpAnalysisSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETRESIDEXPANALYSIS_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, DETRESIDEXPANALYSIS_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
            psFree(rowSet);
            return false;
        }
    }

    // output to fits
    if (!psFitsWriteTable(fits, NULL, rowSet, DETRESIDEXPANALYSIS_TABLE_NAME)) {
        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
        psFree(rowSet);
        return false;
    }

    psFree(rowSet);

    return true;
}

psMetadata *detResidExpAnalysisMetadataFromObject(const detResidExpAnalysisRow *object)
{
    psMetadata      *md;

    md = psMetadataAlloc();
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
        psFree(md);
        return NULL;
    }
    if (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, object->accept)) {
        psError(PS_ERR_UNKNOWN, false, "failed to add item accept");
        psFree(md);
        return NULL;
    }

    return md;
}

detResidExpAnalysisRow *detResidExpAnalysisObjectFromMetadata(psMetadata *md)
{
    bool            status;
    psS32           det_id;
    psS32           iteration;
    char            *exp_id;
    char            *stats;
    char            *recipe;
    bool            accept;

    det_id = psMetadataLookupS32(&status, md, "det_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
        return false;
    }
    iteration = psMetadataLookupS32(&status, md, "iteration");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
        return false;
    }
    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
        return false;
    }
    stats = psMetadataLookupPtr(&status, md, "stats");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
        return false;
    }
    recipe = psMetadataLookupPtr(&status, md, "recipe");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
        return false;
    }
    accept = psMetadataLookupBool(&status, md, "accept");
    if (!status) {
        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item accept");
        return false;
    }

    return detResidExpAnalysisRowAlloc(det_id, iteration, exp_id, stats, recipe, accept);
}
psArray *detResidExpAnalysisSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
{
    psArray         *rowSet;
    psArray         *returnSet;
    psU64           i;

    rowSet = psDBSelectRows(dbh, DETRESIDEXPANALYSIS_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, DETRESIDEXPANALYSIS_INDEX_NAME)) {
            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDEXPANALYSIS_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++) {
        psArrayAdd(returnSet, 0, detResidExpAnalysisObjectFromMetadata(rowSet->data[i]));
    }

    psFree(rowSet);

    return returnSet;
}
long long detResidExpAnalysisDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
{
    long long       deleted = 0;

    for (long long i = 0; i < objects->n; i++) {
        detResidExpAnalysisRow *object = objects->data[i];
        psMetadata *where = detResidExpAnalysisMetadataFromObject(object);
        long long count = psDBDeleteRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, where, limit);
        psFree(where)
        if (count < 0) {
            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detResidExpAnalysis");
            return count;
        }

        deleted += count;
    }

    return deleted;
}
