/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*-
 * ex: set softtabstop=4 tabstop=8 expandtab shiftwidth=4: *
 * Editor Settings: expandtabs and use 4 spaces for indentation */

/*
 * Copyright Likewise Software
 * All rights reserved.
 *
 * 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; either version 2 of the License, or (at
 * your option) any later version.
 *
 * 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 this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * LIKEWISE SOFTWARE MAKES THIS SOFTWARE AVAILABLE UNDER OTHER LICENSING
 * TERMS AS WELL.  IF YOU HAVE ENTERED INTO A SEPARATE LICENSE AGREEMENT
 * WITH LIKEWISE SOFTWARE, THEN YOU MAY ELECT TO USE THE SOFTWARE UNDER THE
 * TERMS OF THAT SOFTWARE LICENSE AGREEMENT INSTEAD OF THE TERMS OF THE GNU
 * GENERAL PUBLIC LICENSE, NOTWITHSTANDING THE ABOVE NOTICE.  IF YOU
 * HAVE QUESTIONS, OR WISH TO REQUEST A COPY OF THE ALTERNATE LICENSING
 * TERMS OFFERED BY LIKEWISE SOFTWARE, PLEASE CONTACT LIKEWISE SOFTWARE AT
 * license@likewisesoftware.com
 */

/*
 * Copyright (C) Likewise Software. All rights reserved.
 *
 * Module Name:
 *
 *        samdbupgrade.c
 *
 * Abstract:
 *
 *
 *      Likewise SAM Database Provider
 *
 *      Database version upgrade routines
 *
 * Authors: Rafal Szczesniak (rafal@likewise.com)
 *
 */

#include "includes.h"


typedef struct _SAMDB_RECORD
{
    LW_LIST_LINKS Record;
    LONG64 RecordId;
    LONG64 NewRecordId;

    LW_LIST_ENTRY Fields;

} SAMDB_RECORD, *PSAMDB_RECORD;


typedef struct _SAMDB_RECORD_FIELD
{
    LW_LIST_LINKS Field;
    PCSTR Name;
    SAMDB_ATTR_TYPE Type;
    PVOID Value;
    PFN_FREE_ATTRIBUTE_VALUE FreeFn;

} SAMDB_RECORD_FIELD, *PSAMDB_RECORD_FIELD;


static
DWORD
SamDbUpgradeVersion3ToVersion4(
    PSAMDB_CONTEXT DbContext
    );

static
DWORD
SamDbFetchRecordsFromTable(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PSAMDB_RECORD* Records
    );

static
DWORD
SamDbSaveRecordsToTable(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PSAMDB_RECORD Records
    );

static
DWORD
SamDbSaveRecordToTable(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PLW_LIST_ENTRY Fields
    );

static
DWORD
SamDbBindRecordValues(
    PSAMDB_CONTEXT DbContext,
    sqlite3_stmt* SqlStatement,
    PLW_LIST_ENTRY Fields
    );

static
VOID
SamDbFreeRecords(
    PSAMDB_RECORD Records
    );

static
VOID
SamDbFreeFields(
    PLW_LIST_ENTRY Fields
    );

static
DWORD
SamDbCreateRecordField(
    PLW_LIST_LINKS Fields,
    PCSTR Name,
    SAMDB_ATTR_TYPE Type,
    PVOID Value
    );

static
DWORD
SamDbGetRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name,
    PSAMDB_ATTR_TYPE Type,
    PVOID* Value
    );

static
DWORD
SamDbSetRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name,
    SAMDB_ATTR_TYPE Type,
    PVOID Value
    );

static
VOID
SamDbRemoveRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name
    );

static
DWORD
SamDbGetRecordIdBySid(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PCSTR ObjectSid,
    PLONG64 RecordId
    );

static inline
DWORD
SamDbMapObjectClassVersion3ToVersion4(
    LONG64 ObjectClass,
    PLONG64 NewObjectClass,
    PBOOLEAN Remove
    );

static
DWORD
SamDbFindRecordById(
    PSAMDB_RECORD Records,
    LONG64 RecordId,
    PSAMDB_RECORD* Record
    );

static
DWORD
SamDbSetDatabaseVersion(
    PSAMDB_CONTEXT DbContext,
    DWORD Version
    );


DWORD
SamDbCheckDatabaseVersion(
    PSAMDB_CONTEXT DbContext,
    PDWORD Version
    )
{
    DWORD error = ERROR_SUCCESS;
    PCSTR sqlStatementString = "SELECT Version FROM " SAM_DB_CONFIG_TABLE ";";
    sqlite3_stmt* sqlStatement = NULL;
    DWORD versionColIndex = 0;
    DWORD version = 0;

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_ROW)
    {
        error = ERROR_SUCCESS;
    }
    else if (error == SQLITE_DONE)
    {
        // There has to be exactly one record in config table
        error = ERROR_INTERNAL_ERROR;

        SAMDB_LOG_ERROR("No records found in the config table");
        goto cleanup;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u or %u was expected",
                        error,
                        SQLITE_ROW,
                        SQLITE_DONE);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

    version = (DWORD)sqlite3_column_int(
                          sqlStatement,
                          versionColIndex);
cleanup:
    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    *Version = version;

    return error;
}


typedef
DWORD
(*UPGRADE_FUNCTION)(
    PSAMDB_CONTEXT DbContext
    );


DWORD
SamDbUpgradeDatabase(
    PSAMDB_CONTEXT DbContext,
    DWORD FromVersion
    )
{
    DWORD error = ERROR_SUCCESS;
    DWORD upgradeIndex = 0;

    struct _UPGRADE_FUNCTIONS {
        DWORD FromVersion;
        DWORD ToVersion;
        UPGRADE_FUNCTION UpgradeFn;
    } upgrades[] = {
        {
            3,
            SAM_DB_SCHEMA_VERSION,
            SamDbUpgradeVersion3ToVersion4
        }
    };

    for (upgradeIndex = 0;
         upgradeIndex < sizeof(upgrades)/sizeof(upgrades[0]);
         upgradeIndex++)
    {
        if (upgrades[upgradeIndex].FromVersion == FromVersion &&
            upgrades[upgradeIndex].ToVersion == SAM_DB_SCHEMA_VERSION)
        {
            error = upgrades[upgradeIndex].UpgradeFn(DbContext);
        }
    }

    return error;
}


static
DWORD
SamDbUpgradeVersion3ToVersion4(
    PSAMDB_CONTEXT DbContext
    )
{
    DWORD error = ERROR_SUCCESS;
    PSAMDB_RECORD objectRecords = NULL;
    PSAMDB_RECORD membershipRecords = NULL;
    PSTR createObjectsTableStatement = NULL;
    PSTR createMembersTableStatement = NULL;
    PCSTR tmpObjectsTableName = SAM_DB_OBJECTS_TABLE "_upgrade";
    PCSTR tmpMembersTableName = SAM_DB_MEMBERS_TABLE "_upgrade";
    PSTR errorMessage = NULL;
    PLW_LIST_LINKS record = NULL;
    PSAMDB_RECORD objectRecord = NULL;
    PSAMDB_RECORD membershipRecord = NULL;
    PLONG64 pObjectClass = NULL;
    PLONG64 pLikewiseFlags = NULL;
    PSTR* pUnixHomeDirectory = NULL;
    PLONG64 pGroupRecordId = NULL;
    PLONG64 pMemberRecordId = NULL;
    LONG64 primaryGroupId = 0;
    LONG64 primaryGroupRid = 0;
    PLONG64 pPrimaryGroupRid = NULL;
    PLONG64 pUserAccountControl = NULL;
    PSTR renameObjectsTableStatement = NULL;
    PSTR renameMembersTableStatement = NULL;

    error = SamDbFetchRecordsFromTable(
                             DbContext,
                             SAM_DB_OBJECTS_TABLE,
                             &objectRecords);
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbFetchRecordsFromTable(
                             DbContext,
                             SAM_DB_MEMBERS_TABLE,
                             &membershipRecords);
    BAIL_ON_SAMDB_ERROR(error);

    error = LwAllocateStringPrintf(
                    &createObjectsTableStatement,
                    SAMDB_QUERY_CREATE_OBJECTS_TABLE,
                    tmpObjectsTableName);
    BAIL_ON_SAMDB_ERROR(error);

    SAMDB_LOG_INFO("Creating new objects table for upgrade.");

    SAMDB_LOG_DEBUG("Statement used to create new objects table:\n%s",
                    createObjectsTableStatement);

    error = sqlite3_exec(
                    DbContext->DbHandle,
                    createObjectsTableStatement,
                    NULL,
                    NULL,
                    &errorMessage);
    if (error)
    {
        SAMDB_LOG_ERROR("Error when creating new objects table "
                        "for upgrade: %s",
                        errorMessage);
    }
    BAIL_ON_SAMDB_ERROR(error);

    error = LwAllocateStringPrintf(
                    &createMembersTableStatement,
                    SAMDB_QUERY_CREATE_MEMBERS_TABLE,
                    tmpMembersTableName);
    BAIL_ON_SAMDB_ERROR(error);

    SAMDB_LOG_INFO("Creating new members table for upgrade.");

    SAMDB_LOG_DEBUG("Statement used to create new members table:\n%s",
                    createMembersTableStatement);

    error = sqlite3_exec(
                    DbContext->DbHandle,
                    createMembersTableStatement,
                    NULL,
                    NULL,
                    &errorMessage);
    if (error)
    {
        SAMDB_LOG_ERROR("Error when creating new members table "
                        "for upgrade: %s",
                        errorMessage);
    }
    BAIL_ON_SAMDB_ERROR(error);

    //
    // Upgrade samdb object records (samdbobjects contents)
    //
    record = LwListTraverse(&objectRecords->Record, record);
    while (record)
    {
        SAMDB_ATTR_TYPE type = 0;
        PVOID value = NULL;
        LONG64 objectClass = 0;
        BOOLEAN removeRecord = FALSE;

        objectRecord = LW_STRUCT_FROM_FIELD(
                                 record,
                                 SAMDB_RECORD,
                                 Record);

        error = SamDbGetRecordField(
                        &objectRecord->Fields,
                        SAM_DB_COL_OBJECT_CLASS,
                        &type,
                        &value);
        BAIL_ON_SAMDB_ERROR(error);

        if (type == SAMDB_ATTR_TYPE_INT64)
        {
            objectClass = *((PLONG64)value);

            //
            // Map old object class to new value
            //
            error = SamDbMapObjectClassVersion3ToVersion4(
                            objectClass,
                            &objectClass,
                            &removeRecord);
            BAIL_ON_SAMDB_ERROR(error);

            //
            // Convert only the objects that are not going to be removed
            //
            if (!removeRecord)
            {
                error = LwAllocateMemory(
                                sizeof(*pObjectClass),
                                OUT_PPVOID(&pObjectClass));
                BAIL_ON_SAMDB_ERROR(error);

                *pObjectClass = objectClass;

                error = SamDbSetRecordField(
                                &objectRecord->Fields,
                                SAM_DB_COL_OBJECT_CLASS,
                                SAMDB_ATTR_TYPE_INT64,
                                pObjectClass);
                BAIL_ON_SAMDB_ERROR(error);

                pObjectClass = NULL;
            }
        }
        else
        {
            error = ERROR_INTERNAL_ERROR;

            SAMDB_LOG_ERROR("Invalid type of %s field found",
                            SAM_DB_COL_OBJECT_CLASS);

            goto cleanup;
        }

        if (objectClass == (LONG64)SAMDB_OBJECT_CLASS_USER ||
            objectClass == (LONG64)SAMDB_OBJECT_CLASS_LOCAL_GROUP)
        {
            //
            // Add LikewiseFlags field with default flag set
            //
            error = LwAllocateMemory(
                            sizeof(*pLikewiseFlags),
                            OUT_PPVOID(&pLikewiseFlags));
            BAIL_ON_SAMDB_ERROR(error);

            *pLikewiseFlags = SAMDB_LIKEWISE_FLAG_UNIX_ENABLED;

            error = SamDbCreateRecordField(
                            &objectRecord->Fields,
                            SAM_DB_COL_LIKEWISE_FLAGS,
                            SAMDB_ATTR_TYPE_INT64,
                            pLikewiseFlags);
            BAIL_ON_SAMDB_ERROR(error);

            pLikewiseFlags = NULL;
        }

        if (objectClass == (LONG64)SAMDB_OBJECT_CLASS_USER)
        {
            //
            // Create unix home directory with a copy of current
            // home directory field
            //
            error = SamDbGetRecordField(
                            &objectRecord->Fields,
                            "Homedir",
                            &type,
                            &value);
            BAIL_ON_SAMDB_ERROR(error);

            if (type == SAMDB_ATTR_TYPE_TEXT)
            {
                PSTR* pStrValue = (PSTR*)value;

                error = LwAllocateMemory(
                                sizeof(*pUnixHomeDirectory),
                                OUT_PPVOID(&pUnixHomeDirectory));
                BAIL_ON_SAMDB_ERROR(error);

                error = LwAllocateString(
                                *pStrValue,
                                pUnixHomeDirectory);
                BAIL_ON_SAMDB_ERROR(error);

                error = SamDbCreateRecordField(
                                &objectRecord->Fields,
                                SAM_DB_COL_UNIX_HOME_DIR,
                                SAMDB_ATTR_TYPE_TEXT,
                                pUnixHomeDirectory);
                BAIL_ON_SAMDB_ERROR(error);

                pUnixHomeDirectory = NULL;
            }
            else if (type == SAMDB_ATTR_TYPE_UNKNOWN)
            {
                error = SamDbCreateRecordField(
                                &objectRecord->Fields,
                                SAM_DB_COL_UNIX_HOME_DIR,
                                SAMDB_ATTR_TYPE_UNKNOWN,
                                NULL);
                BAIL_ON_SAMDB_ERROR(error);
            }
            else
            {
                error = ERROR_INTERNAL_ERROR;

                SAMDB_LOG_ERROR("Invalid type of Homedir field found");
                goto cleanup;
            }
        }

        //
        // Remove fields which no longer exist in samdb schema V4
        //
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "DistinguishedName");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "ParentDN");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "NetBIOSName");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "CommonName");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "PrimaryDomain");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "Role");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "ForcePasswordChange");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "AllowPasswordChange");
        SamDbRemoveRecordField(
                        &objectRecord->Fields,
                        "ForceLogoffTime");

        if (objectClass == (LONG64)SAMDB_OBJECT_CLASS_USER)
        {
            //
            // Map old primary group id to primary group RID
            //
            error = SamDbGetRecordField(
                            &objectRecord->Fields,
                            "PrimaryGroup",
                            &type,
                            &value);
            BAIL_ON_SAMDB_ERROR(error);

            if (type == SAMDB_ATTR_TYPE_INT64)
            {
                primaryGroupId = *((PLONG64)value);
                primaryGroupRid = primaryGroupId - SAM_DB_UID_RID_OFFSET;

                error = LwAllocateMemory(
                                  sizeof(*pObjectClass),
                                  OUT_PPVOID(&pPrimaryGroupRid));
                BAIL_ON_SAMDB_ERROR(error);

                *pPrimaryGroupRid = primaryGroupRid;

                error = SamDbSetRecordField(
                                  &objectRecord->Fields,
                                  SAM_DB_COL_PRIMARY_GROUP,
                                  SAMDB_ATTR_TYPE_INT64,
                                  pPrimaryGroupRid);
                BAIL_ON_SAMDB_ERROR(error);

                pPrimaryGroupRid = NULL;
            }
            else
            {
                error = ERROR_INTERNAL_ERROR;

                SAMDB_LOG_ERROR("Invalid type of PrimaryGroup field found");

                goto cleanup;
            }
        }

        if (objectClass == (LONG64)SAMDB_OBJECT_CLASS_USER)
        {
            //
            // Map old account flags to new user account control flags
            //
            error = SamDbGetRecordField(
                            &objectRecord->Fields,
                            "AccountFlags",
                            &type,
                            &value);
            BAIL_ON_SAMDB_ERROR(error);

            if (type == SAMDB_ATTR_TYPE_INT64)
            {
                LONG64 accountFlags = *((PLONG64)value);
                SAMDB_USER_FLAGS remainingFlags = 0;

                error = LwAllocateMemory(
                                  sizeof(*pUserAccountControl),
                                  OUT_PPVOID(&pUserAccountControl));
                BAIL_ON_SAMDB_ERROR(error);

                *pUserAccountControl = SamDbMapSamrToDatabaseFlags(
                                            (SAMDB_USER_FLAGS)accountFlags,
                                            &remainingFlags);
                if (remainingFlags)
                {
                    error = ERROR_INTERNAL_ERROR;

                    SAMDB_LOG_ERROR("Remaining unmapped account flag(s) 0x%08x "
                                    "found while upgrading the database."
                                    "Stopping the upgrade process.",
                                    (DWORD)remainingFlags);
                    goto cleanup;
                }

                error = SamDbSetRecordField(
                                  &objectRecord->Fields,
                                  SAM_DB_COL_ACCOUNT_FLAGS,
                                  SAMDB_ATTR_TYPE_INT64,
                                  pUserAccountControl);
                BAIL_ON_SAMDB_ERROR(error);

                pUserAccountControl = NULL;
            }
            else
            {
                error = ERROR_INTERNAL_ERROR;

                SAMDB_LOG_ERROR("Invalid type of AccountFlags field found");

                goto cleanup;
            }
        }

        if (removeRecord)
        {
            //
            // Remove no longer used object
            // (e.g. old SAMDB_OBJECT_CLASS_CONTAINER)
            //
            PLW_LIST_LINKS nextRecord = NULL;

            nextRecord = LwListTraverse(
                                 &objectRecords->Record,
                                 record);
            LwListRemove(record);

            SamDbFreeFields(&objectRecord->Fields);
            LW_SAFE_FREE_MEMORY(objectRecord);

            record = nextRecord;
        }
        else
        {
            record = LwListTraverse(
                             &objectRecords->Record,
                             record);
        }
    }

    //
    // Save upgraded object records
    //
    error = SamDbSaveRecordsToTable(
                             DbContext,
                             tmpObjectsTableName,
                             objectRecords);
    BAIL_ON_SAMDB_ERROR(error);

    //
    // Upgrade samdb membership records (samdbmembers contents)
    //
    record = LwListTraverse(&membershipRecords->Record, record);
    while (record)
    {
        PLONG64 groupRecordId = NULL;
        PLONG64 memberRecordId = NULL;
        SAMDB_ATTR_TYPE type = SAMDB_ATTR_TYPE_UNKNOWN;

        membershipRecord = LW_STRUCT_FROM_FIELD(
                                      record,
                                      SAMDB_RECORD,
                                      Record);

        error = SamDbGetRecordField(
                             &membershipRecord->Fields,
                             "GroupRecordId",
                             &type,
                             (PVOID*)&groupRecordId);
        BAIL_ON_SAMDB_ERROR(error);

        if (type == SAMDB_ATTR_TYPE_INT64)
        {
            error = LwAllocateMemory(
                                 sizeof(*pGroupRecordId),
                                 OUT_PPVOID(&pGroupRecordId));
            BAIL_ON_SAMDB_ERROR(error);

            error = SamDbFindRecordById(
                                 objectRecords,
                                 *groupRecordId,
                                 &objectRecord);
            BAIL_ON_SAMDB_ERROR(error);

            *pGroupRecordId = objectRecord->NewRecordId;

            error = SamDbSetRecordField(
                                 &membershipRecord->Fields,
                                 "GroupRecordId",
                                 type,
                                 pGroupRecordId);
            BAIL_ON_SAMDB_ERROR(error);

            pGroupRecordId = NULL;
        }
        else
        {
            error = ERROR_INTERNAL_ERROR;

            SAMDB_LOG_ERROR("Invalid type found in GroupRecordId field");
            goto cleanup;
        }

        objectRecord = NULL;

        error = SamDbGetRecordField(
                             &membershipRecord->Fields,
                             "MemberRecordId",
                             &type,
                             (PVOID*)&memberRecordId);
        BAIL_ON_SAMDB_ERROR(error);

        if (type == SAMDB_ATTR_TYPE_INT64)
        {
            error = LwAllocateMemory(
                                 sizeof(*pMemberRecordId),
                                 OUT_PPVOID(&pMemberRecordId));
            BAIL_ON_SAMDB_ERROR(error);

            error = SamDbFindRecordById(
                                 objectRecords,
                                 *memberRecordId,
                                 &objectRecord);
            BAIL_ON_SAMDB_ERROR(error);

            *pMemberRecordId = objectRecord->NewRecordId;

            error = SamDbSetRecordField(
                                 &membershipRecord->Fields,
                                 "MemberRecordId",
                                 type,
                                 pMemberRecordId);
            BAIL_ON_SAMDB_ERROR(error);

            pMemberRecordId = NULL;
        }
        else
        {
            error = ERROR_INTERNAL_ERROR;
            SAMDB_LOG_ERROR("Invalid type found in MemberRecordId field");
            goto cleanup;
        }

        record = LwListTraverse(&membershipRecords->Record, record);
    }

    //
    // Save upgraded object records
    //
    error = SamDbSaveRecordsToTable(
                             DbContext,
                             tmpMembersTableName,
                             membershipRecords);
    BAIL_ON_SAMDB_ERROR(error);

    //
    // Delete old tables
    //
    error = sqlite3_exec(
                    DbContext->DbHandle,
                    "DROP TABLE " SAM_DB_OBJECTS_TABLE ";",
                    NULL,
                    NULL,
                    &errorMessage);
    if (error)
    {
        SAMDB_LOG_ERROR("Error while deleting old objects table: %s",
                        errorMessage);
        error = ERROR_INTERNAL_ERROR;

        BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);
        goto cleanup;
    }

    error = sqlite3_exec(
                    DbContext->DbHandle,
                    "DROP TABLE " SAM_DB_MEMBERS_TABLE ";",
                    NULL,
                    NULL,
                    &errorMessage);
    if (error)
    {
        SAMDB_LOG_ERROR("Error while deleting old members table: %s",
                        errorMessage);
        error = ERROR_INTERNAL_ERROR;

        BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);
        goto cleanup;
    }

    //
    // Rename upgraded tables
    //
    error = LwAllocateStringPrintf(
                    &renameObjectsTableStatement,
                    "ALTER TABLE %s RENAME TO " SAM_DB_OBJECTS_TABLE ";",
                    tmpObjectsTableName);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_exec(
                    DbContext->DbHandle,
                    renameObjectsTableStatement,
                    NULL,
                    NULL,
                    &errorMessage);
    if (error)
    {
        SAMDB_LOG_ERROR("Error while executing table rename (%s): %s",
                        renameObjectsTableStatement,
                        errorMessage);
        error = ERROR_INTERNAL_ERROR;

        BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);
        goto cleanup;
    }

    error = LwAllocateStringPrintf(
                    &renameMembersTableStatement,
                    "ALTER TABLE %s RENAME TO " SAM_DB_MEMBERS_TABLE ";",
                    tmpMembersTableName);
    BAIL_ON_SAMDB_ERROR(error);

    if (error)
    {
        SAMDB_LOG_ERROR("Error while executing table rename (%s): %s",
                        renameMembersTableStatement,
                        errorMessage);
        error = ERROR_INTERNAL_ERROR;

        BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);
        goto cleanup;
    }

    error = sqlite3_exec(
                    DbContext->DbHandle,
                    renameMembersTableStatement,
                    NULL,
                    NULL,
                    &errorMessage);
    if (error)
    {
        SAMDB_LOG_ERROR("Error while executing table rename (%s): %s",
                        renameMembersTableStatement,
                        errorMessage);
        error = ERROR_INTERNAL_ERROR;

        BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);
        goto cleanup;
    }

    //
    // Finally, set the new database version
    //
    error = SamDbSetDatabaseVersion(
                    DbContext,
                    SAM_DB_SCHEMA_VERSION);
    BAIL_ON_SAMDB_ERROR(error);

cleanup:
    if (error)
    {
        if (errorMessage)
        {
            sqlite3_free(errorMessage);
        }
    }

    if (objectRecords)
    {
        SamDbFreeRecords(objectRecords);
    }

    if (membershipRecords)
    {
        SamDbFreeRecords(membershipRecords);
    }

    LW_SAFE_FREE_MEMORY(createObjectsTableStatement);
    LW_SAFE_FREE_MEMORY(createMembersTableStatement);
    LW_SAFE_FREE_MEMORY(pObjectClass);
    LW_SAFE_FREE_MEMORY(pLikewiseFlags);
    LW_SAFE_FREE_MEMORY(pGroupRecordId);
    LW_SAFE_FREE_MEMORY(pMemberRecordId);
    LW_SAFE_FREE_MEMORY(pUnixHomeDirectory);
    LW_SAFE_FREE_MEMORY(pUserAccountControl);
    LW_SAFE_FREE_MEMORY(renameObjectsTableStatement);
    LW_SAFE_FREE_MEMORY(renameMembersTableStatement);

    return error;
}


static
DWORD
SamDbFetchRecordsFromTable(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PSAMDB_RECORD* Records
    )
{
    DWORD error = ERROR_SUCCESS;
    PCSTR sqlSelectAllStatementStringFmt =
        "SELECT * FROM %s;";
    PSTR sqlSelectAllStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    DWORD sqliteStatus = 0;
    DWORD columnsCount = 0;
    DWORD columnIndex = 0;
    PSAMDB_RECORD recordsHead = NULL;
    PSAMDB_RECORD record = NULL;
    PSAMDB_RECORD_FIELD field = NULL;

    error = LwAllocateMemory(
                      sizeof(*recordsHead),
                      OUT_PPVOID(&recordsHead));
    BAIL_ON_SAMDB_ERROR(error);

    LwListInit(&recordsHead->Record);

    error = LwAllocateStringPrintf(
                      &sqlSelectAllStatementString,
                      sqlSelectAllStatementStringFmt,
                      TableName);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlSelectAllStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlSelectAllStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_ROW ||
        error == SQLITE_DONE)
    {
        sqliteStatus = error;
        error = ERROR_SUCCESS;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u or %u was expected",
                        error,
                        SQLITE_ROW,
                        SQLITE_DONE);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

    while (sqliteStatus == SQLITE_ROW)
    {
        error = LwAllocateMemory(
                            sizeof(*record),
                            OUT_PPVOID(&record));
        BAIL_ON_SAMDB_ERROR(error);

        LwListInit(&record->Record);
        LwListInit(&record->Fields);

        columnsCount = sqlite3_column_count(sqlStatement);
        for (columnIndex = 0;
             columnIndex < columnsCount;
             columnIndex++)
        {
            PVOID fieldValue = NULL;
            PLONG64 int64Value = NULL;
            PSTR* pStrValue = NULL;
            PSTR strValue = NULL;
            PSAMDB_ATTRIBUTE_VALUE_BLOB blobValue = NULL;
            SAMDB_ATTR_TYPE fieldType = 0;
            PSTR fieldName = NULL;
            const void* blob = NULL;
            int columnType = 0;
            const char* columnName = NULL;

            columnName = sqlite3_column_name(
                               sqlStatement,
                               columnIndex);

            error = LwAllocateString(
                               columnName,
                               &fieldName);
            BAIL_ON_SAMDB_ERROR(error);

            columnType = sqlite3_column_type(
                               sqlStatement,
                               columnIndex);
            switch (columnType)
            {
            case SQLITE_INTEGER:
                fieldType = SAMDB_ATTR_TYPE_INT64;

                error = LwAllocateMemory(
                                  sizeof(*int64Value),
                                  OUT_PPVOID(&int64Value));
                BAIL_ON_SAMDB_ERROR(error);

                *int64Value = sqlite3_column_int64(
                                  sqlStatement,
                                  columnIndex);
                fieldValue = int64Value;
                break;

            case SQLITE_TEXT:
                fieldType = SAMDB_ATTR_TYPE_TEXT;

                error = LwAllocateMemory(
                                  sizeof(*pStrValue),
                                  OUT_PPVOID(&pStrValue));
                BAIL_ON_SAMDB_ERROR(error);

                strValue = (PSTR)sqlite3_column_text(
                                  sqlStatement,
                                  columnIndex);

                error = LwAllocateString(
                                  strValue,
                                  pStrValue);
                BAIL_ON_SAMDB_ERROR(error);

                fieldValue = pStrValue;
                break;

            case SQLITE_BLOB:
                fieldType = SAMDB_ATTR_TYPE_BLOB;

                error = LwAllocateMemory(
                                  sizeof(*blobValue),
                                  OUT_PPVOID(&blobValue));
                BAIL_ON_SAMDB_ERROR(error);

                blobValue->Length = sqlite3_column_bytes(
                                  sqlStatement,
                                  columnIndex);
                error = LwAllocateMemory(
                                  blobValue->Length,
                                  OUT_PPVOID(&blobValue->Value));
                BAIL_ON_SAMDB_ERROR(error);

                blob = sqlite3_column_blob(
                                  sqlStatement,
                                  columnIndex);

                RtlCopyMemory(blobValue->Value,
                              blob,
                              blobValue->Length);

                fieldValue = blobValue;
                break;

            case SQLITE_NULL:
                // Unknown attribute type means NULL value here
                fieldValue = NULL;
                break;
            }

            error = SamDbCreateRecordField(
                               &record->Fields,
                               fieldName,
                               fieldType,
                               fieldValue);
            BAIL_ON_SAMDB_ERROR(error);

            if (RtlCStringIsEqual(
                               fieldName,
                               SAM_DB_COL_RECORD_ID,
                               TRUE))
            {
                record->RecordId = *int64Value;
            }

            field = NULL;
            fieldValue = NULL;
            int64Value = NULL;
            pStrValue = NULL;
            blobValue = NULL;
        }

        LwListInsertAfter(&recordsHead->Record, &record->Record);
        record = NULL;

        error = sqlite3_step(sqlStatement);
        if (error == SQLITE_ROW ||
            error == SQLITE_DONE)
        {
            sqliteStatus = error;
            error = ERROR_SUCCESS;
        }
        else
        {
            SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                            "while %u or %u was expected",
                            error,
                            SQLITE_ROW,
                            SQLITE_DONE);
            BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
        }
    }

cleanup:
    if (error)
    {
        if (recordsHead)
        {
            SamDbFreeRecords(recordsHead);
        }
    }

    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    LW_SAFE_FREE_MEMORY(sqlSelectAllStatementString);

    *Records = recordsHead;

    return error;
}


static
DWORD
SamDbSaveRecordsToTable(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PSAMDB_RECORD Records
    )
{
    DWORD error = ERROR_SUCCESS;
    PLW_LIST_LINKS record = NULL;
    PSAMDB_RECORD objectRecord = NULL;

    while ((record = LwListTraverse(&Records->Record, record)) != NULL)
    {
        SAMDB_ATTR_TYPE type = 0;
        PSTR* pObjectSid = NULL;
        LONG64 newRecordId = 0;

        objectRecord = LW_STRUCT_FROM_FIELD(
                                 record,
                                 SAMDB_RECORD,
                                 Record);

        error = SamDbSaveRecordToTable(
                             DbContext,
                             TableName,
                             &objectRecord->Fields);
        BAIL_ON_SAMDB_ERROR(error);

        error = SamDbGetRecordField(
                             &objectRecord->Fields,
                             SAM_DB_COL_OBJECT_SID,
                             &type,
                             (PVOID*)&pObjectSid);
        if (error == ERROR_SUCCESS)
        {
            if (type == SAMDB_ATTR_TYPE_TEXT)
            {
                error = SamDbGetRecordIdBySid(
                                     DbContext,
                                     TableName,
                                     *pObjectSid,
                                     &newRecordId);
                BAIL_ON_SAMDB_ERROR(error);

                objectRecord->NewRecordId = newRecordId;
            }
            else
            {
                error = ERROR_INTERNAL_ERROR;

                SAMDB_LOG_ERROR("Invalid type of %s field found",
                                SAM_DB_COL_OBJECT_SID);

                goto cleanup;
            }
        }
        else if (error == ERROR_FILE_NOT_FOUND)
        {
            // This record doesn't have a SID
            error = ERROR_SUCCESS;
        }
        else if (error != ERROR_SUCCESS)
        {
            BAIL_ON_SAMDB_ERROR(error);
        }
    }

cleanup:
    return error;
}


#define SAMDB_SQL_INSERT_OBJECT_PREFIX     "INSERT INTO %s ("
#define SAMDB_SQL_INSERT_OBJECT_VALUES     "VALUES ("


static
DWORD
SamDbSaveRecordToTable(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PLW_LIST_ENTRY Fields
    )
{
    DWORD error = ERROR_SUCCESS;
    size_t sqlStatementSize = 256;
    size_t sqlStatementLength = 0;
    size_t maxSqlStatementSize = 4096;
    PSTR sqlStatementString = NULL;
    PLW_LIST_ENTRY field = NULL;
    PLW_LIST_ENTRY firstField = Fields;
    PSAMDB_RECORD_FIELD dbField = NULL;
    PSTR format = NULL;
    DWORD fieldIndex = 0;
    sqlite3_stmt* sqlStatement = NULL;

    //
    // Create SQL statement string (INSERT INTO samdbobjects ...)
    //
    error = LwAllocateMemory(
                    sizeof(sqlStatementString[0]) * sqlStatementSize,
                    OUT_PPVOID(&sqlStatementString));
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    SAMDB_SQL_INSERT_OBJECT_PREFIX,
                    TableName);
    BAIL_ON_SAMDB_ERROR(error);

    format = "%s,";

    while ((field = LwListTraverse(Fields, field)) != NULL)
    {
        dbField = LW_STRUCT_FROM_FIELD(
                                 field,
                                 SAMDB_RECORD_FIELD,
                                 Field);

        //
        // Don't append "," if this is the last field
        //
        if (field == firstField->Prev)
        {
            format = "%s) ";
        }

        error = SamDbPrintfString(
                        &sqlStatementString,
                        &sqlStatementSize,
                        &sqlStatementLength,
                        maxSqlStatementSize,
                        format,
                        dbField->Name
                        );
        BAIL_ON_SAMDB_ERROR(error);
    }

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    SAMDB_SQL_INSERT_OBJECT_VALUES);
    BAIL_ON_SAMDB_ERROR(error);

    format = "?%u,";

    while ((field = LwListTraverse(Fields, field)) != NULL)
    {
        dbField = LW_STRUCT_FROM_FIELD(
                                 field,
                                 SAMDB_RECORD_FIELD,
                                 Field);

        //
        // Append terminating ");" if this is the last field
        //
        if (field == firstField->Prev)
        {
            format = "?%u);";
        }

        error = SamDbPrintfString(
                        &sqlStatementString,
                        &sqlStatementSize,
                        &sqlStatementLength,
                        maxSqlStatementSize,
                        format,
                        ++fieldIndex);
        BAIL_ON_SAMDB_ERROR(error);
    }

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

    error = SamDbBindRecordValues(
                      DbContext,
                      sqlStatement,
                      Fields);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_DONE)
    {
        error = ERROR_SUCCESS;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u was expected",
                        error,
                        SQLITE_DONE);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

cleanup:
    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    return error;
}


static
DWORD
SamDbBindRecordValues(
    PSAMDB_CONTEXT DbContext,
    sqlite3_stmt* SqlStatement,
    PLW_LIST_ENTRY Fields
    )
{
    DWORD error = ERROR_SUCCESS;
    PLW_LIST_LINKS field = NULL;
    PSAMDB_RECORD_FIELD dbField = NULL;
    DWORD fieldIndex = 0;
    PSTR* pStrValue = NULL;
    PLONG64 pInt64Value = NULL;
    PSAMDB_ATTRIBUTE_VALUE_BLOB pBlob = NULL;

    while ((field = LwListTraverse(Fields, field)) != NULL)
    {
        fieldIndex++;

        dbField = LW_STRUCT_FROM_FIELD(
                                 field,
                                 SAMDB_RECORD_FIELD,
                                 Field);
        switch (dbField->Type)
        {
        case SAMDB_ATTR_TYPE_TEXT:
            pStrValue = (PSTR*)dbField->Value;
            error = sqlite3_bind_text(
                                SqlStatement,
                                fieldIndex,
                                (const char*)(*pStrValue),
                                -1,
                                SQLITE_TRANSIENT);
            break;

        case SAMDB_ATTR_TYPE_INT64:
            pInt64Value = (PLONG64)dbField->Value;
            error = sqlite3_bind_int64(
                                SqlStatement,
                                fieldIndex,
                                (sqlite3_int64)(*pInt64Value));
            break;

        case SAMDB_ATTR_TYPE_BLOB:
            pBlob = (PSAMDB_ATTRIBUTE_VALUE_BLOB)dbField->Value;
            error = sqlite3_bind_blob(
                                SqlStatement,
                                fieldIndex,
                                (const void*)pBlob->Value,
                                (int)pBlob->Length,
                                SQLITE_TRANSIENT);
            break;

        case SAMDB_ATTR_TYPE_UNKNOWN:
            // Unknown attribute type means NULL value here
            error = sqlite3_bind_null(
                                SqlStatement,
                                fieldIndex);
            break;

        default:
            error = ERROR_INTERNAL_ERROR;
            SAMDB_LOG_ERROR("Invalid type found in record field");
            goto cleanup;
        }

        if (error)
        {
            SAMDB_LOG_ERROR("Error code %u returned from sqlite3_bind_* ",
                            error);

            error = ERROR_INTERNAL_ERROR;
            goto cleanup;
        }
    }

cleanup:
    return error;
}


static
VOID
SamDbFreeRecords(
    PSAMDB_RECORD Records
    )
{
    PLW_LIST_ENTRY record = NULL;
    PSAMDB_RECORD dbRecord = NULL;

    record = LwListTraverse(&Records->Record, record);
    while (record)
    {
        dbRecord = LW_STRUCT_FROM_FIELD(
                             record,
                             SAMDB_RECORD,
                             Record);
        SamDbFreeFields(&dbRecord->Fields);

        record = LwListTraverse(&Records->Record, record);
        LW_SAFE_FREE_MEMORY(dbRecord);
    }
}


static
VOID
SamDbFreeFields(
    PLW_LIST_ENTRY Fields
    )
{
    PLW_LIST_ENTRY field = NULL;
    PSAMDB_RECORD_FIELD dbField = NULL;

    field = LwListTraverse(Fields, field);
    while (field)
    {
        dbField = LW_STRUCT_FROM_FIELD(
                             field,
                             SAMDB_RECORD_FIELD,
                             Field);

        if (dbField->FreeFn &&
            dbField->Value)
        {
            dbField->FreeFn(dbField->Value);
        }

        field = LwListTraverse(Fields, field);
        LW_SAFE_FREE_MEMORY(dbField);
    }
}


static
DWORD
SamDbCreateRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name,
    SAMDB_ATTR_TYPE Type,
    PVOID Value
    )
{
    DWORD error = ERROR_SUCCESS;
    PSAMDB_RECORD_FIELD field = NULL;

    error = LwAllocateMemory(
                      sizeof(*field),
                      OUT_PPVOID(&field));
    BAIL_ON_SAMDB_ERROR(error);

    LwListInit(&field->Field);

    field->Name = Name;
    field->Type   = Type;
    field->Value  = Value;

    switch (Type)
    {
    case SAMDB_ATTR_TYPE_TEXT:
        field->FreeFn = SamDbAttributeFreeString;
        break;

    case SAMDB_ATTR_TYPE_INT64:
        field->FreeFn = SamDbAttributeFreePointer;
        break;

    case SAMDB_ATTR_TYPE_BLOB:
        field->FreeFn = SamDbAttributeFreeBlob;
        break;

    case SAMDB_ATTR_TYPE_UNKNOWN:
        // Unknown attribute type means NULL value here
        field->FreeFn = NULL;
        field->Value = NULL;
        break;

    default:
        error = ERROR_INTERNAL_ERROR;
        SAMDB_LOG_ERROR("Invalid type found in record field");
        goto cleanup;
    }

    LwListInsertAfter(Fields, &field->Field);

cleanup:
    if (error)
    {
        if (field)
        {
            LwListRemove(&field->Field);
            LW_SAFE_FREE_MEMORY(field);
        }
    }

    return error;
}


static
DWORD
SamDbGetRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name,
    PSAMDB_ATTR_TYPE Type,
    PVOID* Value
    )
{
    DWORD error = ERROR_SUCCESS;
    PLW_LIST_ENTRY field = NULL;
    PSAMDB_RECORD_FIELD dbField = NULL;
    SAMDB_ATTR_TYPE type = SAMDB_ATTR_TYPE_UNKNOWN;
    PVOID value = NULL;
    BOOLEAN found = FALSE;

    field = LwListTraverse(Fields, field);
    while (field)
    {
        dbField = LW_STRUCT_FROM_FIELD(
                             field,
                             SAMDB_RECORD_FIELD,
                             Field);

        if (RtlCStringIsEqual(
                     dbField->Name,
                     Name,
                     TRUE))
        {
            type = dbField->Type;
            value = dbField->Value;

            found = TRUE;
            break;
        }

        field = LwListTraverse(Fields, field);
    }

    if (!found)
    {
        error = ERROR_FILE_NOT_FOUND;
    }

    *Type = type;
    *Value = value;

    return error;
}


static
DWORD
SamDbSetRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name,
    SAMDB_ATTR_TYPE Type,
    PVOID Value
    )
{
    DWORD error = ERROR_SUCCESS;
    PLW_LIST_ENTRY field = NULL;
    PSAMDB_RECORD_FIELD dbField = NULL;
    BOOLEAN found = FALSE;

    field = LwListTraverse(Fields, field);
    while (field)
    {
        dbField = LW_STRUCT_FROM_FIELD(
                             field,
                             SAMDB_RECORD_FIELD,
                             Field);

        if (RtlCStringIsEqual(
                     dbField->Name,
                     Name,
                     TRUE))
        {
            //
            // Remove old value before setting new
            //
            if (dbField->FreeFn &&
                dbField->Value)
            {
                dbField->FreeFn(
                            dbField->Value);
            }

            dbField->Value = Value;
            dbField->Type  = Type;

            switch (Type)
            {
            case SAMDB_ATTR_TYPE_TEXT:
                dbField->FreeFn = SamDbAttributeFreeString;
                break;

            case SAMDB_ATTR_TYPE_INT64:
                dbField->FreeFn = SamDbAttributeFreePointer;
                break;

            case SAMDB_ATTR_TYPE_BLOB:
                dbField->FreeFn = SamDbAttributeFreeBlob;
                break;

            case SAMDB_ATTR_TYPE_UNKNOWN:
                // Unknown attribute type means NULL value here
                dbField->FreeFn = NULL;
                dbField->Value = NULL;
                break;

            default:
                error = ERROR_INTERNAL_ERROR;
                SAMDB_LOG_ERROR("Invalid type found in record field");
                goto cleanup;
            }

            found = TRUE;
            break;
        }

        field = LwListTraverse(Fields, field);
    }

    if (!found)
    {
        error = ERROR_FILE_NOT_FOUND;
    }

cleanup:
    return error;
}


static
VOID
SamDbRemoveRecordField(
    PLW_LIST_ENTRY Fields,
    PCSTR Name
    )
{
    PLW_LIST_ENTRY field = NULL;
    PSAMDB_RECORD_FIELD dbField = NULL;


    while ((field = LwListTraverse(Fields, field)) != NULL)
    {
        dbField = LW_STRUCT_FROM_FIELD(
                             field,
                             SAMDB_RECORD_FIELD,
                             Field);

        if (RtlCStringIsEqual(
                     dbField->Name,
                     Name,
                     TRUE))
        {
            LwListRemove(field);

            if (dbField->FreeFn &&
                dbField->Value)
            {
                dbField->FreeFn(dbField->Value);
            }

            LW_SAFE_FREE_MEMORY(dbField);
            break;
        }
    }
}


static
DWORD
SamDbGetRecordIdBySid(
    PSAMDB_CONTEXT DbContext,
    PCSTR TableName,
    PCSTR ObjectSid,
    PLONG64 RecordId
    )
{
    DWORD error = ERROR_SUCCESS;
    PCSTR sqlStatementStringFormat =
        "SELECT " SAM_DB_COL_RECORD_ID " "
        "FROM %s WHERE " SAM_DB_COL_OBJECT_SID " = ?1;";
    PSTR sqlStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    LONG64 recordId = 0;

    error = LwAllocateStringPrintf(
                      &sqlStatementString,
                      sqlStatementStringFormat,
                      TableName);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

    error = sqlite3_bind_text(
                     sqlStatement,
                     1,
                     ObjectSid,
                     -1,
                     SQLITE_TRANSIENT);
    BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_ROW)
    {
        error = ERROR_SUCCESS;
    }
    else if (error == SQLITE_DONE)
    {
        error = ERROR_FILE_NOT_FOUND;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u was expected",
                        error,
                        SQLITE_ROW);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

    recordId = sqlite3_column_int64(
                        sqlStatement,
                        0);

cleanup:
    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    *RecordId = recordId;

    return error;
}


static inline
DWORD
SamDbMapObjectClassVersion3ToVersion4(
    LONG64 ObjectClass,
    PLONG64 NewObjectClass,
    PBOOLEAN Remove
    )
{
    DWORD error = ERROR_SUCCESS;
    BOOLEAN remove = FALSE;
    LONG64 objectClass = 0;

    switch (ObjectClass)
    {
    case 1: // SAMDB_OBJECT_CLASS_DOMAIN v3
        objectClass = SAMDB_OBJECT_CLASS_DOMAIN;
        break;

    case 2: // SAMDB_OBJECT_CLASS_BUILTIN_DOMAIN v3
        objectClass = SAMDB_OBJECT_CLASS_DOMAIN;
        break;

    case 3: // SAMDB_OBJECT_CLASS_CONTAINER v3
        remove = TRUE;
        break;

    case 4: // SAMDB_OBJECT_CLASS_LOCAL_GROUP v3
        objectClass = SAMDB_OBJECT_CLASS_LOCAL_GROUP;
        break;

    case 5: // SAMDB_OBJECT_CLASS_USER v3
        objectClass = SAMDB_OBJECT_CLASS_USER;
        break;

    case 6: // SAMDB_OBJECT_CLASS_LOCALGRP_MEMBER v3
        objectClass = SAMDB_OBJECT_CLASS_FOREIGN_SECURITY_PRINCIPAL;
        break;

    default:
        error = ERROR_INTERNAL_ERROR;

        SAMDB_LOG_ERROR("Invalid object class encountered in samdbobjects record");
        goto cleanup;
    }

cleanup:
    *NewObjectClass = objectClass;
    *Remove = remove;

    return error;
}


static
DWORD
SamDbFindRecordById(
    PSAMDB_RECORD Records,
    LONG64 RecordId,
    PSAMDB_RECORD* Record
    )
{
    DWORD error = ERROR_SUCCESS;
    PLW_LIST_LINKS record = NULL;
    PSAMDB_RECORD objectRecord = NULL;
    PSAMDB_RECORD foundRecord = NULL;

    while ((record = LwListTraverse(&Records->Record, record)) != NULL)
    {
        objectRecord = LW_STRUCT_FROM_FIELD(
                                 record,
                                 SAMDB_RECORD,
                                 Record);
        if (objectRecord->RecordId == RecordId)
        {
            foundRecord = objectRecord;
            break;
        }
    }

    if (foundRecord == NULL)
    {
        error = ERROR_FILE_NOT_FOUND;
    }

    *Record = foundRecord;

    return error;
}


static
DWORD
SamDbSetDatabaseVersion(
    PSAMDB_CONTEXT DbContext,
    DWORD Version
    )
{
    DWORD error = ERROR_SUCCESS;
    PSTR sqlStatementString =
        "UPDATE " SAM_DB_CONFIG_TABLE " SET Version = ?1;";
    sqlite3_stmt* sqlStatement = NULL;

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

    error = sqlite3_bind_int(
                      sqlStatement,
                      1,
                      (int)Version);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_DONE)
    {
        error = ERROR_SUCCESS;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u was expected",
                        error,
                        SQLITE_DONE);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

cleanup:
    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    return error;
}

