SQLite Forum

Custom error messages for required table-valued function parameter
Login
I have a number of table-valued functions that require one or more parameters, and I'd like to give a helpful error message if a required parameter is not specified. I've tried setting sqlite3_vtab*'s zErrMsg, but it's ignored when returning SQLITE_CONSTRAINT, which is what the documentation says should be returned for this case. (If I return anything else, it makes the query planner unhappy with at least some queries involving this function.)

Specifically, the docs say:

    The SQLITE_CONSTRAINT return is useful for table-valued functions that have required parameters. If the aConstraint[].usable field is false for one of the required parameter, then the xBestIndex method should return SQLITE_CONSTRAINT.

(from [section 2.3.3](https://sqlite.org/vtab.html))

Below is sample source that reproduces the problem. It shows a nice error message when the parameter passed is wrong, but only a generic "no query solution" when the required parameter is missing. (Near the end, change "SELECT count(*) FROM testvtab(NULL);" to "SELECT count(*) FROM testvtab();" to see the case where a custom error message cannot be provided back to the application.)

Could SQLite add support for setting the error message when all solutions fail with SQLITE_CONSTRAINT? I recognize there are multiple possible invocations to xBestIndex in this case, and it would have to choose one of them (say, the first or the last that failed with SQLITE_CONSTRAINT), but which one wouldn't matter for my purposes.

Thanks,

David

```c
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "sqlite3.h"

enum test_vtab_column
{
    test_vtab_column_value = 0,
    test_vtab_column_source
};

static int testVTabConnect(sqlite3* connection, void* pAux, int argc, const char* const* argv, sqlite3_vtab** ppVtab,
    char** pzErr)
{
    (void)pAux;
    (void)argc;
    (void)argv;

    sqlite3_vtab* table = (sqlite3_vtab*)sqlite3_malloc(sizeof(*table));

    if (table == NULL)
    {
        return SQLITE_NOMEM;
    }

    memset(table, 0, sizeof(*table));

    int rc = sqlite3_declare_vtab(connection, "CREATE TABLE x("
        "Value INTEGER PRIMARY KEY, "
        "Source HIDDEN INTEGER NOT NULL);");

    if (rc != SQLITE_OK)
    {
        sqlite3_free(table);

        const char* connectionError = sqlite3_errmsg(connection);
        size_t errorLength = strlen(connectionError);
        char* errorMessageCopy = (char*)sqlite3_malloc64((sqlite3_uint64)errorLength + 1);

        if (errorMessageCopy == NULL)
        {
            return SQLITE_NOMEM;
        }

        strncpy_s(errorMessageCopy, errorLength + 1, connectionError, errorLength);
        *pzErr = errorMessageCopy;
        return rc;
    }

    *ppVtab = table;
    return SQLITE_OK;
}

static int testVTabDisconnect(sqlite3_vtab* pVtab)
{
    sqlite3_free(pVtab);
    return SQLITE_OK;
}

static bool try_set_error_message(sqlite3_vtab* table, const char* message)
{
    size_t length = strlen(message);
    char* copy = (char*)sqlite3_malloc64((sqlite3_uint64)length + 1);

    if (copy == NULL)
    {
        return false;
    }

    strncpy_s(copy, length + 1, message, length);

    sqlite3_free(table->zErrMsg);
    table->zErrMsg = copy;
    return true;
}

static int testVTabBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo)
{
    int sourceDataConstraintIndex = -1;

    for (int index = 0; index < pIdxInfo->nConstraint; ++index)
    {
        struct sqlite3_index_constraint* constraint = &pIdxInfo->aConstraint[index];

        if (!constraint->usable)
        {
            continue;
        }

        if (constraint->iColumn == test_vtab_column_source && constraint->op == SQLITE_INDEX_CONSTRAINT_EQ)
        {
            sourceDataConstraintIndex = index;
        }
    }

    if (sourceDataConstraintIndex == -1)
    {
        if (!try_set_error_message(tab, "testvtab requires a Source value"))
        {
            return SQLITE_NOMEM;
        }

        return SQLITE_CONSTRAINT;
    }

    if (pIdxInfo->nOrderBy == 1 && pIdxInfo->aOrderBy[0].iColumn == test_vtab_column_value &&
        pIdxInfo->aOrderBy[0].desc == 0)
    {
        pIdxInfo->orderByConsumed = 1;
    }

    pIdxInfo->idxNum = 0;
    pIdxInfo->idxStr = NULL;

    pIdxInfo->aConstraintUsage[sourceDataConstraintIndex].argvIndex = 1;
    pIdxInfo->aConstraintUsage[sourceDataConstraintIndex].omit = 1;

    pIdxInfo->estimatedCost = 1;

    return SQLITE_OK;
}

struct test_vtab_cursor
{
    sqlite3_vtab_cursor base; // This base struct must come first.
    sqlite_int64 rowid;
    sqlite_int64 source;
};

typedef struct test_vtab_cursor test_vtab_cursor;

static int testVTabOpen(sqlite3_vtab* p, sqlite3_vtab_cursor** ppCursor)
{
    (void)p;

    test_vtab_cursor* cursor = (test_vtab_cursor*)sqlite3_malloc(sizeof(*cursor));

    if (cursor == NULL)
    {
        return SQLITE_NOMEM;
    }

    memset(cursor, 0, sizeof(*cursor));

    *ppCursor = &cursor->base;

    return SQLITE_OK;
}

static int testVTabClose(sqlite3_vtab_cursor* cur)
{
    test_vtab_cursor* cursor = (test_vtab_cursor*)cur;
    sqlite3_free(cursor);
    return SQLITE_OK;
}

static int testVTabFilter(sqlite3_vtab_cursor* pVtabCursor, int idxNum, const char* idxStr, int argc,
    sqlite3_value** argv)
{
    assert(idxNum == 0);
    (void)idxNum;
    assert(idxStr == NULL);
    (void)idxStr;
    assert(argc == 1);
    (void)argc;

    test_vtab_cursor* cursor = (test_vtab_cursor*)pVtabCursor;

    sqlite3_vtab* table = cursor->base.pVtab;
    sqlite3_value* arg = argv[0];

    if (sqlite3_value_type(arg) != SQLITE_INTEGER)
    {
        return try_set_error_message(table, "testvtab requires a Source value that is an INTEGER") ?
            SQLITE_ERROR : SQLITE_NOMEM;
    }

    cursor->rowid = 0;
    cursor->source = sqlite3_value_int64(arg);

    return SQLITE_OK;
}

static int testVTabNext(sqlite3_vtab_cursor* cur)
{
    test_vtab_cursor* cursor = (test_vtab_cursor*)cur;
    ++(cursor->rowid);
    return SQLITE_OK;
}

static int testVTabEof(sqlite3_vtab_cursor* cur)
{
    test_vtab_cursor* cursor = (test_vtab_cursor*)cur;
    return cursor->rowid < cursor->source ? 0 : 1;
}

static int testVTabColumn(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int i)
{
    test_vtab_cursor* cursor = (test_vtab_cursor*)cur;

    assert(i <= test_vtab_column_source);

    switch (i)
    {
    case test_vtab_column_value:
        sqlite3_result_int64(ctx, cursor->rowid);
        return SQLITE_OK;
    default:
        assert(i == test_vtab_column_source);
        sqlite3_result_int64(ctx, cursor->source);
        return SQLITE_OK;
    }
}

static int testVTabRowid(sqlite3_vtab_cursor* cur, sqlite_int64* pRowid)
{
    test_vtab_cursor* cursor = (test_vtab_cursor*)cur;
    *pRowid = cursor->rowid;
    return SQLITE_OK;
}

static sqlite3_module testVTabModule = {
  3, // iVersion
  NULL, // xCreate
  testVTabConnect, // xConnect
  testVTabBestIndex, // xBestIndex
  testVTabDisconnect, //xDisconnect
  NULL, // xDestroy
  testVTabOpen, //int xOpen
  testVTabClose, //int xClose
  testVTabFilter, //int xFilter
  testVTabNext, // int xNext
  testVTabEof, // int xEof
  testVTabColumn, // xColumn
  testVTabRowid, // xRowid
  NULL, // xUpdate
  NULL, // xBegin
  NULL, // xSync
  NULL, // xCommit
  NULL, // xRollback
  NULL, // xFindFunction
  NULL, // xRename
  NULL, // xSavepoint
  NULL, // xRelease
  NULL, // xRollbackTo
  NULL, // xShadowName
};

int main()
{
    int rc = 0;
    int rcCleanup = 0;
    int rcCurrentCleanup = 0;
    sqlite3* db = NULL;
    sqlite3_stmt* stmt = NULL;

    rc = sqlite3_open("D:\\test.db", &db);

    if (rc)
    {
        goto cleanup;
    }

    rc = sqlite3_create_module_v2(db, "testvtab", &testVTabModule, NULL, NULL);

    if (rc)
    {
        goto cleanup;
    }

    rc = sqlite3_prepare_v2(db, "SELECT count(*) FROM testvtab(NULL);", -1, &stmt, NULL);

    if (rc)
    {
        goto cleanup;
    }

    rc = sqlite3_step(stmt);

    if (rc != SQLITE_ROW)
    {
        goto cleanup;
    }

    printf("%i\n", sqlite3_column_int(stmt, 0));

    rc = sqlite3_step(stmt);

    if (rc != SQLITE_DONE)
    {
        goto cleanup;
    }

    rc = SQLITE_OK;

cleanup:
    if (rc && db)
    {
        fprintf(stderr, "%s\n", sqlite3_errmsg(db));
    }

    if (stmt)
    {
        rcCurrentCleanup = sqlite3_finalize(stmt);

        if (rcCurrentCleanup && !rcCleanup)
        {
            rcCleanup = rcCurrentCleanup;
        }
    }

    if (db)
    {
        rcCurrentCleanup = sqlite3_close_v2(db);

        if (rcCurrentCleanup && !rcCleanup)
        {
            rcCleanup = rcCurrentCleanup;
        }
    }

    if (rcCleanup)
    {
        return rcCleanup;
    }

    return rc;
}
```