/*
 * pstamptool.c
 *
 * Copyright (C) 2008  Joshua Hoblitt
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * program; see the file COPYING. If not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>

#include "pxtools.h"
#include "pxdata.h"
#include "pstamptool.h"

static bool adddatastoreMode(pxConfig *config);
static bool datastoreMode(pxConfig *config);
static bool moddatastoreMode(pxConfig *config);
static bool addReqMode(pxConfig *config);
static bool listReqMode(pxConfig *config);
static bool pendingReqMode(pxConfig *config);
static bool processedReqMode(pxConfig *config);
static bool addJobMode(pxConfig *config);
static bool listJobMode(pxConfig *config);
static bool pendingJobMode(pxConfig *config);
static bool processedJobMode(pxConfig *config);

# define MODECASE(caseName, func) \
    case caseName: \
    if (!func(config)) { \
                goto FAIL; \
            } \
    break;


int main(int argc, char **argv)
{
    psLibInit(NULL);

    pxConfig *config = pstamptoolConfig(NULL, argc, argv);
    if (!config) {
        psError(PXTOOLS_ERR_CONFIG, false, "failed to configure");
        goto FAIL;
    }

    switch (config->mode) {
        MODECASE(PSTAMPTOOL_MODE_ADDDATASTORE, adddatastoreMode);
        MODECASE(PSTAMPTOOL_MODE_DATASTORE, datastoreMode);
        MODECASE(PSTAMPTOOL_MODE_MODDATASTORE, moddatastoreMode);
        MODECASE(PSTAMPTOOL_MODE_ADDREQ, addReqMode);
        MODECASE(PSTAMPTOOL_MODE_LISTREQ, listReqMode);
        MODECASE(PSTAMPTOOL_MODE_PENDINGREQ, pendingReqMode);
        MODECASE(PSTAMPTOOL_MODE_PROCESSEDREQ, processedReqMode);
        MODECASE(PSTAMPTOOL_MODE_ADDJOB, addJobMode);
        MODECASE(PSTAMPTOOL_MODE_LISTJOB, listJobMode);
        MODECASE(PSTAMPTOOL_MODE_PENDINGJOB, pendingJobMode);
        MODECASE(PSTAMPTOOL_MODE_PROCESSEDJOB, processedJobMode);
        default:
            psAbort("invalid option (this should not happen)");
    }

    psFree(config);
    pmConfigDone();
    psLibFinalize();

    exit(EXIT_SUCCESS);

FAIL:
    psErrorStackPrint(stderr, "\n");
    int exit_status = pxerrorGetExitStatus();

    psFree(config);
    pmConfigDone();
    psLibFinalize();

    exit(exit_status);
}

static bool adddatastoreMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psString uri = psMetadataLookupStr(&status, config->args, "-uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -uri");
        return false;
    }
    if (!uri) {
        psError(PS_ERR_UNKNOWN, true, "-uri is required");
        return false;
    }
    psString outProduct = psMetadataLookupStr(&status, config->args, "-out_product");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to looku value for -out_product");
        return false;
    }
    if (!outProduct) {
        psError(PS_ERR_UNKNOWN, true, "-out_product is required");
        return false;
    }

    psString lastFileset = psMetadataLookupStr(&status, config->args, "-last_fileset");
    psString state = psMetadataLookupStr(&status, config->args, "-state");
    if (!state) {
        state = "enabled";
    }

    if (!pstampDataStoreInsert(config->dbh,
            0,
            state,
            lastFileset,
            outProduct,
            uri
        )) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    return true;
}

static bool datastoreMode(pxConfig *config)
{
    bool status = false;
    PS_ASSERT_PTR_NON_NULL(config, false);

    psString ds_id = psMetadataLookupStr(&status, config->args, "-ds_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -ds_id");
        return false;
    }

    psString query = NULL;
    psStringAppend(&query, "SELECT * FROM pstampDataStore");
    if (ds_id) {
        psStringAppend(&query, " WHERE ds_id = '%s'", ds_id);
    }
    if (!p_psDBRunQuery(config->dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("pstamptool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            return false;
        }
    }

    // negative simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "pstampDataStore", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}
static bool moddatastoreMode(pxConfig *config)
{
    bool    status;

    PS_ASSERT_PTR_NON_NULL(config, false);

    psString ds_id = psMetadataLookupStr(&status, config->args, "-ds_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -ds_id");
        return false;
    }
    if (!ds_id) {
        psError(PS_ERR_UNKNOWN, true, "-ds_id is required");
        return false;
    }
    psString lastFileset = psMetadataLookupStr(&status, config->args, "-last_fileset");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -last_fileset");
        return false;
    }
    psString state = psMetadataLookupStr(&status, config->args, "-state");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -state");
        return false;
    }

    if (!state && !lastFileset) {
        psError(PS_ERR_UNKNOWN, true, "at least one of -last_fileset or -state is required");
        return false;
    }

    char *query = NULL;
    bool needComma = false;
    
    psStringAppend(&query, "UPDATE pstampDataStore SET");

    if (lastFileset) {
        psStringAppend(&query, " lastFileset = '%s'", lastFileset);
        needComma = true;
    }

    if (state) {
        psStringAppend(&query, "%s state = '%s'", needComma ? "," : " ",
            state);
        needComma = true; // be ready in case we add another field
    }
                
    psStringAppend(&query, " WHERE ds_id = '%s'", ds_id);

    if (!p_psDBRunQuery(config->dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }

    psU64 affected = psDBAffectedRows(config->dbh);
    if (affected != 1) {
        psError(PS_ERR_UNKNOWN, false, "should have affected one row but %" 
                                        PRIu64 " rows were modified", affected);
        return false;
    }

    return true;
}

#define  MAGIC_STR  "$REQ_ID"

// Replace $REQ_ID with the value of the request id if present in the outFileset

static bool
updateFilesetID(pxConfig *config, psS64 req_id, psString outFileset)
{
    char *magic;

    magic = strcasestr(outFileset, MAGIC_STR);
    if (magic == NULL) {
        return true;
    }

    // find part of the string after the magic
    char *after = magic + strlen(MAGIC_STR);

    if (*after == 0) {
        after = NULL;
    }

    psString converted = NULL;
    if (magic > outFileset) {
        int len = magic - outFileset;
        converted = psStringAlloc(len + 1);
        strncpy(converted, outFileset, len);
        converted[len] = 0;
    }

    psStringAppend(&converted, "%" PRId64, req_id);
    if (after) {
        psStringAppend(&converted, after);
    }

    if (!p_psDBRunQuery(config->dbh, "UPDATE pstampRequest set outFileset = '%s' WHERE req_id = %" PRId64,
            converted, req_id)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    return true;
}

static bool addReqMode(pxConfig *config)
{
    bool    status;

    PS_ASSERT_PTR_NON_NULL(config, false);

    psString uri = psMetadataLookupStr(&status, config->args, "-uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -uri");
        return false;
    }
    if (!uri) {
        psError(PS_ERR_UNKNOWN, true, "-uri is required");
        return false;
    }
    psString outFileset = psMetadataLookupStr(&status, config->args, "-out_fileset");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -out_fileset");
        return false;
    }
    if (!outFileset) {
        psError(PS_ERR_UNKNOWN, true, "-out_fileset is required");
        return false;
    }

    // Data Store ID is optional
    psString ds_id = psMetadataLookupStr(&status, config->args, "-ds_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -ds_id");
        return false;
    }
    if (!ds_id) {
        ds_id = "0";
    }

    char *query ="INSERT INTO pstampRequest"
                 " (state, uri, ds_id, outFileset, resultsFile)"
                 " VALUES( 'new', '%s', %s, '%s', 1)";

    if (!p_psDBRunQuery(config->dbh, query, uri, ds_id, outFileset)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    psU64 affected = psDBAffectedRows(config->dbh);
    if (affected != 1) {
        psError(PS_ERR_UNKNOWN, false, 
            "should have affected one row but %" PRIu64 " rows were modified",
            affected);
        return false;
    }

    psS64 req_id = psDBLastInsertID(config->dbh);
    if (!updateFilesetID(config, req_id, outFileset)) {
        psError(PS_ERR_UNKNOWN, false, "failed to update fileset id\n");
        return false;
    }

    printf("%" PRId64 "\n", req_id);

    return true;
}

static bool pendingReqMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psU64 limit = psMetadataLookupU64(&status, config->args, "-limit");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -limit");
        return false;
    }

    psString query = psStringCopy(
            "SELECT"
            " *"
            " FROM pstampRequest"
            " WHERE state = 'new'"
        );

    if (config->where) {
        psString whereClause = psDBGenerateWhereConditionSQL(config->where, "pstampRequest");
        psStringAppend(&query, " AND %s", whereClause);
        psFree(whereClause);
    }

    // treat limit == 0 as "no limit"
    if (limit) {
        psString limitString = psDBGenerateLimitSQL(limit);
        psStringAppend(&query, " %s", limitString);
        psFree(limitString);
    }

    if (!p_psDBRunQuery(config->dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }
    psFree(query);

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("pstamptool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            psFree(output);
            return false;
        }
    }

    // negative simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "pstampRequest", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}
static bool listReqMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psU64 limit = psMetadataLookupU64(&status, config->args, "-limit");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -limit");
        return false;
    }

    psString query = NULL;

    psString req_id = psMetadataLookupStr(&status, config->args, "-req_id");
    
    if (req_id) {
        psStringAppend(&query,
                "SELECT"
                " *"
                " FROM pstampRequest"
                " WHERE req_id = %s", req_id
            );
    } else {
        fprintf(stderr, "req_id must be specified\n");
        exit (1);
    }

    // treat limit == 0 as "no limit"
    if (limit) {
        psString limitString = psDBGenerateLimitSQL(limit);
        psStringAppend(&query, " %s", limitString);
        psFree(limitString);
    }

    if (!p_psDBRunQuery(config->dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }
    psFree(query);

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("pstamptool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            psFree(output);
            return false;
        }
    }

    // negative simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "pstampRequest", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}

static bool processedReqMode(pxConfig *config)
{
    bool    status;

    PS_ASSERT_PTR_NON_NULL(config, false);

    psString req_id = psMetadataLookupStr(&status, config->args, "-req_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -req_id");
        return false;
    }
    if (!req_id) {
        psError(PS_ERR_UNKNOWN, true, "-req_id is required");
        return false;
    }
    psString state = psMetadataLookupStr(&status, config->args, "-state");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -state");
        return false;
    }
    if (!state) {
        psError(PS_ERR_UNKNOWN, true, "-state is required");
        return false;
    }

    // XXX: check state for a legal value

    char *query ="UPDATE pstampRequest"
                 " SET state = '%s'"
                 " WHERE req_id = '%s'";

    if (!p_psDBRunQuery(config->dbh, query, state, req_id)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }

    psU64 affected = psDBAffectedRows(config->dbh);
    if (affected != 1) {
        psError(PS_ERR_UNKNOWN, false, "should have affected one row but %" 
                                        PRIu64 " rows were modified", affected);
        return false;
    }

    return true;
}
static bool addJobMode(pxConfig *config)
{
    bool    status;
    bool    stampJob = false;

    PS_ASSERT_PTR_NON_NULL(config, false);

    psString job_type = psMetadataLookupStr(&status, config->args, "-job_type");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -job_type");
        return false;
    }
    if (job_type) {
        if (!strcmp(job_type, "get_image")) {
            stampJob = false;
        } else if (!strcmp(job_type, "stamp")) {
            stampJob = true;
        } else {
            psError(PS_ERR_UNKNOWN, false, "unknown value for -job_type: %s", job_type);
            return false;
        }
    } else {
        job_type = "stamp";
        stampJob = true;
    }

    psString uri = psMetadataLookupStr(&status, config->args, "-uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -uri");
        return false;
    }
    if (!uri) {
        psError(PS_ERR_UNKNOWN, true, "-uri is required");
        return false;
    }

    psString req_id = psMetadataLookupStr(&status, config->args, "-req_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -req_id");
        return false;
    }
    if (!req_id) {
        psError(PS_ERR_UNKNOWN, true, "-req_id is required");
        return false;
    }

    psString outputBase = psMetadataLookupStr(&status, config->args, "-outputBase");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -outputBase");
        return false;
    }
    if (!outputBase) {
        psError(PS_ERR_UNKNOWN, true, "-outputBase is required");
        return false;
    }

    psString argString = psMetadataLookupStr(&status, config->args, "-args");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -args");
        return false;
    }
    if (!argString) {
        if (stampJob) {
            psError(PS_ERR_UNKNOWN, true, "-args is required");
            return false;
        }
    }

    char *query;
    
    if (stampJob) {
        query ="INSERT INTO pstampJob"
                 " (req_id, state, jobType, uri, outputBase, args)"
                 " VALUES( %s, 'run', '%s', '%s', '%s', '%s')";
    } else {
        query ="INSERT INTO pstampJob"
                 " (req_id, state, jobType, uri, outputBase)"
                 " VALUES( %s, 'run', '%s', '%s', '%s')";
    }

    if (!p_psDBRunQuery(config->dbh, query, req_id, job_type, uri, outputBase, argString)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    psU64 affected = psDBAffectedRows(config->dbh);
    if (affected != 1) {
        psError(PS_ERR_UNKNOWN, false, 
            "should have affected one row but %" PRIu64 " rows were modified",
            affected);
        return false;
    }

    psS64 job_id = psDBLastInsertID(config->dbh);
    printf("%" PRId64 "\n", job_id);

    return true;
}

static bool listJobMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psU64 limit = psMetadataLookupU64(&status, config->args, "-limit");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -limit");
        return false;
    }

    psString query = NULL;

    psString req_id = psMetadataLookupStr(&status, config->args, "-req_id");
    psString job_id = psMetadataLookupStr(&status, config->args, "-job_id");
    
    if (req_id) {
        psStringAppend(&query,
                "SELECT"
                " *"
                " FROM pstampJob"
                " WHERE req_id = %s", req_id
            );
    } else if (job_id) {
        psStringAppend(&query,
                "SELECT"
                " *"
                " FROM pstampJob"
                " WHERE job_id = %s", job_id
            );
    } else {
        fprintf(stderr, "either req_id or job_id must be specified\n");
        exit (1);
    }

    // treat limit == 0 as "no limit"
    if (limit) {
        psString limitString = psDBGenerateLimitSQL(limit);
        psStringAppend(&query, " %s", limitString);
        psFree(limitString);
    }

    if (!p_psDBRunQuery(config->dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }
    psFree(query);

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("pstamptool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            psFree(output);
            return false;
        }
    }

    // negative simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "pstampJob", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}

static bool pendingJobMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psU64 limit = psMetadataLookupU64(&status, config->args, "-limit");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -limit");
        return false;
    }

    psString query = psStringCopy(
            "SELECT"
            " *"
            " FROM pstampJob"
            " WHERE state = 'run'"
        );

    if (config->where) {
        psString whereClause = psDBGenerateWhereConditionSQL(config->where, "pstampJob");
        psStringAppend(&query, " AND %s", whereClause);
        psFree(whereClause);
    }

    // treat limit == 0 as "no limit"
    if (limit) {
        psString limitString = psDBGenerateLimitSQL(limit);
        psStringAppend(&query, " %s", limitString);
        psFree(limitString);
    }

    if (!p_psDBRunQuery(config->dbh, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }
    psFree(query);

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("pstamptool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            psFree(output);
            return false;
        }
    }

    // negative simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "pstampJob", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}

static bool processedJobMode(pxConfig *config)
{
    bool    status;

    PS_ASSERT_PTR_NON_NULL(config, false);

    psString job_id = psMetadataLookupStr(&status, config->args, "-job_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -job_id");
        return false;
    }
    if (!job_id) {
        psError(PS_ERR_UNKNOWN, true, "-job_id is required");
        return false;
    }
    psString state = psMetadataLookupStr(&status, config->args, "-state");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -state");
        return false;
    }
    if (!state) {
        psError(PS_ERR_UNKNOWN, true, "-state is required");
        return false;
    }
    psString jobResult = psMetadataLookupStr(&status, config->args, "-result");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -result");
        return false;
    }

    psString resultStr = NULL;
    if (!jobResult) {
        resultStr = psStringCopy("");
    } else {
        psStringAppend(&resultStr, ", result = '%s'", jobResult);
    }
        

    // XXX: check state for a legal value

    char *query ="UPDATE pstampJob"
                 " SET state = '%s' %s"
                 " WHERE job_id = '%s'";

    if (!p_psDBRunQuery(config->dbh, query, state, resultStr, job_id)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(query);
        return false;
    }
    psFree(resultStr);

    psU64 affected = psDBAffectedRows(config->dbh);
    if (affected != 1) {
        psError(PS_ERR_UNKNOWN, false, "should have affected one row but %" 
                                        PRIu64 " rows were modified", affected);
        return false;
    }

    return true;
}
