Want to view this project? In-Memory Record Database
Creating the Edit Functionality
The last bit of functionality we need to add before tying this all together with the console UI is the ability to edit records. We’ll be utilizing a lot of our pre-existing functions to create these edit functions, but let’s think about how we want users to actually use this.
Note that we are NOT allowing users to modify the ID of the record. The only editable fields are first name, last name, and age.
Since we’re not allowing users to create complex queries, we’re going to have to sacrifice some usability. For this program, I’m setting it up to where a user must enter in the ID of the record they would like to modify. This will help prevent some potential errors that we haven’t or have loosely guarded against. It makes it easy on us too because we already have a function that does the heavy lifting for us: findRecordById.
Let’s look at editFirstName:
enum EditResult editFirstName(struct RecordTable *table, int id, const char *newFirstName)
{
struct Record *recordToEdit = findRecordById(table, id);
if (recordToEdit == NULL)
{
return EDIT_FAILED;
}
if (strlen(newFirstName) > 100 || newFirstName == NULL)
{
return EDIT_FAILED;
}
if (strcmp(recordToEdit->firstName, newFirstName) == 0)
{
return EDIT_SUCCESS;
}
char oldFirstName[101];
strcpy(oldFirstName, recordToEdit->firstName);
enum DeleteResult deleteResult = deleteNode(&table->firstNameTree, recordToEdit);
if (deleteResult == DELETE_FAILED)
{
return EDIT_CORRUPTED;
}
strcpy(recordToEdit->firstName, newFirstName);
enum InsertResult insertResult = insert(&table->firstNameTree, recordToEdit);
if (insertResult != INSERT_SUCCESS)
{
strcpy(recordToEdit->firstName, oldFirstName);
enum InsertResult rollbackInsert = insert(&table->firstNameTree, recordToEdit);
if (rollbackInsert != INSERT_SUCCESS)
{
return EDIT_CORRUPTED;
}
return EDIT_FAILED;
}
return EDIT_SUCCESS;
}
As we can see, the very first thing we’re doing is searching for the record by the ID passed to it. If a record doesn’t exist, we simply return the enum EDIT_FAILED and call it a day.
enum EditResult editFirstName(struct RecordTable *table, int id, const char *newFirstName)
{
struct Record *recordToEdit = findRecordById(table, id);
if (recordToEdit == NULL)
{
return EDIT_FAILED;
}
We’re also checking the length of the first name to make sure it meets our constraints. If you remember, we’ve limited first name to only 100 characters. This if statement technically won’t matter since we’ll validate input in the console UI function, but it doesn’t hurt to have.
if (strlen(newFirstName) > 100 || newFirstName == NULL)
{
return EDIT_FAILED;
}
We also want to check to see if the new first name matches the old first name. If so, there’s no reason to continue through the rest of the function and we can simply return the enum EDIT_SUCCESS.
if (strcmp(recordToEdit->firstName, newFirstName) == 0)
{
return EDIT_SUCCESS;
}
Before we go any further though, what are we really doing when we edit a record? That may seem like a trivial question, but think about what we’ve been doing this whole time. We’re utilizing an AVL tree to keep our records nice and tidy and since they have their own set of rules we need to follow, we have to be careful when we go around changing records.
I’ve seen a lot of implementations of different types of trees that cover the basics like insertion and deletion, but not a lot of people really touch on what happens when you try to modify one of the tree leaves. I suppose it’s because most of the implementations you see are simply dealing with int key instead of something like struct Record, so there’s no need to “edit”. Or perhaps many people expect you to simply piece together what you’re supposed to do in this scenario. Either way, I think it’s a shame that the concept is hardly ever touched on.
I complain more about this here if you’re interested in reading it.
Right, so what is editing when it comes to Binary Search Trees? On a high level:
- Find the node you want to edit
- Delete the node from the tree
- Reinsert the modified node
This is what editing actually does in this instance. We do this to keep our records within the constraints of the tree. Let’s visualize this to fully understand why we’re doing this. Take a look at our example tree here:

Here we have a balanced first name tree. Ava is on the left because it’s “less than” Henry and Rob is on the right because it’s “greater than” Henry.
Let’s say that Rob wanted to change his name to Bob because he’s getting older and Uncle Bob sounds more friendly for an old guy. (His real name is Robert, so this could seriously happen. Trust me.) Let’s edit Rob’s name to Bob.

And wouldn’t you know it, we’ve got ourselves a problem. Our tree is unbalanced and now if we were to search for Bob, we’d always return No Results Found. Why? Because Bob is “less than” Henry, so our algorithm will always take the left path. It will never ever in a million years take the right path unless we change the algorithm itself and we’re not going to do that. This is exactly why we delete the node and then reinsert it. Because both of our functions that handle the insert logic and delete logic also rebalance the tree.
So if we were to delete Rob…

Our tree then gets rebalanced. In this case it’s already balanced. Then we’ll add “Rob” back in as Bob…

And finally, rebalance the tree again.

Much better. We’ve successfully “edited” Rob to Bob and stayed within the constraints of our AVL tree.
So let’s come back to our code starting with the delete portion:
char oldFirstName[101];
strcpy(oldFirstName, recordToEdit->firstName);
enum DeleteResult deleteResult = deleteNode(&table->firstNameTree, recordToEdit);
if (deleteResult == DELETE_FAILED)
{
return EDIT_CORRUPTED;
}
First, we’ll save the old first name just in case we need to revert back to it. Remember, our deleteNode only deletes the struct RecordNode* that has a pointer to our struct Record. We can do this safely because we already have a pointer to the struct Record that we’re actually trying to modify. The only reason we have this is because we went searching for it first:
struct Record *recordToEdit = findRecordById(table, id);
So we can say goodbye to the node that contained this pointer and create a new node which will then go through the insert motions that we set up in part 3.
strcpy(recordToEdit->firstName, newFirstName);
enum InsertResult insertResult = insert(&table->firstNameTree, recordToEdit);
if (insertResult != INSERT_SUCCESS)
{
strcpy(recordToEdit->firstName, oldFirstName);
enum InsertResult rollbackInsert = insert(&table->firstNameTree, recordToEdit);
if (rollbackInsert != INSERT_SUCCESS)
{
return EDIT_CORRUPTED;
}
return EDIT_FAILED;
}
return EDIT_SUCCESS;
We’ll begin by actually changing our struct Records first name value to the new first name using strcpy and then call insert. If our insert failed, we’ll copy the old first name back and try to insert again. If that doesn’t work then we’re “cooked” as they say.
This process is repeated for last name and age as well. It’s the same concept you’ve seen here. On that note, we’ve completed functionality for:
- Creating records
- Deleting records
- Editing records
- Searching records
- Listing sorting records
All that’s left is for us to create the console UI and wrap up the program with some cleanup functions!