Case Study 1: Building a Simple Database with Typed Files
The Scenario
Your friend runs a small tutoring business and has been keeping track of contacts in a paper notebook. Names get crossed out, phone numbers get scribbled over, and finding a specific contact means flipping through dozens of pages. She asks you to build a simple digital phone book.
The requirements are straightforward:
- Add a new contact (name, phone number, email)
- Search for a contact by name
- Edit a contact's phone number or email
- Delete a contact
- List all contacts
- Persist data between sessions — closing and reopening the program should not lose any data
This is a classic CRUD application (Create, Read, Update, Delete), and it is a perfect fit for Pascal's typed file system.
Designing the Data Structure
We begin with the contact record. Every field must be a fixed-size type for typed file compatibility:
type
TContact = record
Name: string[40];
Phone: string[15];
Email: string[50];
Deleted: Boolean; { Soft-delete flag }
end;
The Deleted field implements the "mark-as-deleted" strategy discussed in the chapter. Rather than physically removing records (which requires shifting data), we simply flag them. This makes deletion an O(1) operation — seek to the record and flip a Boolean.
Why Not Use an Array?
An array of contacts would work fine during a single session, but it adds complexity: we need to load the entire file into memory at startup and save it all back on exit. With typed files and random access, we can read and write individual records as needed. The file is the database.
For this case study, we will use a hybrid approach: keep an in-memory array for fast access during the session, but save and load from a typed file for persistence.
The Core Implementation
Constants and Global State
const
MAX_CONTACTS = 500;
DB_FILE = 'phonebook.dat';
var
Contacts: array[0..MAX_CONTACTS - 1] of TContact;
ContactCount: Integer;
Loading the Database
procedure LoadDatabase;
var
F: file of TContact;
IOCode: Integer;
begin
ContactCount := 0;
if not FileExists(DB_FILE) then
begin
WriteLn('No existing database found. Starting fresh.');
Exit;
end;
AssignFile(F, DB_FILE);
{$I-}
Reset(F);
IOCode := IOResult;
{$I+}
if IOCode <> 0 then
begin
WriteLn('Warning: Could not open database (error ', IOCode, ').');
WriteLn('Starting with empty contact list.');
Exit;
end;
while (not Eof(F)) and (ContactCount < MAX_CONTACTS) do
begin
Read(F, Contacts[ContactCount]);
Inc(ContactCount);
end;
CloseFile(F);
WriteLn('Loaded ', ContactCount, ' contacts.');
end;
Saving the Database
procedure SaveDatabase;
var
F: file of TContact;
I: Integer;
IOCode: Integer;
begin
AssignFile(F, DB_FILE);
{$I-}
Rewrite(F);
IOCode := IOResult;
{$I+}
if IOCode <> 0 then
begin
WriteLn('ERROR: Could not save database! (error ', IOCode, ')');
Exit;
end;
for I := 0 to ContactCount - 1 do
Write(F, Contacts[I]);
CloseFile(F);
WriteLn('Saved ', ContactCount, ' contacts to ', DB_FILE, '.');
end;
Adding a Contact
procedure AddContact;
var
NewContact: TContact;
begin
if ContactCount >= MAX_CONTACTS then
begin
WriteLn('Database is full! Maximum ', MAX_CONTACTS, ' contacts.');
Exit;
end;
WriteLn;
WriteLn('--- Add New Contact ---');
Write('Name: ');
ReadLn(NewContact.Name);
if NewContact.Name = '' then
begin
WriteLn('Name cannot be empty. Cancelled.');
Exit;
end;
Write('Phone: ');
ReadLn(NewContact.Phone);
Write('Email: ');
ReadLn(NewContact.Email);
NewContact.Deleted := False;
Contacts[ContactCount] := NewContact;
Inc(ContactCount);
WriteLn('Contact added: ', NewContact.Name);
end;
Searching for a Contact
function FindContact(const SearchName: string): Integer;
var
I: Integer;
LowerSearch, LowerName: string;
begin
Result := -1;
LowerSearch := LowerCase(SearchName);
for I := 0 to ContactCount - 1 do
begin
if Contacts[I].Deleted then
Continue;
LowerName := LowerCase(Contacts[I].Name);
if Pos(LowerSearch, LowerName) > 0 then
begin
Result := I;
Exit;
end;
end;
end;
procedure SearchContact;
var
SearchTerm: string;
I: Integer;
Found: Boolean;
LowerSearch, LowerName: string;
begin
WriteLn;
Write('Search for name: ');
ReadLn(SearchTerm);
if SearchTerm = '' then Exit;
LowerSearch := LowerCase(SearchTerm);
Found := False;
for I := 0 to ContactCount - 1 do
begin
if Contacts[I].Deleted then Continue;
LowerName := LowerCase(Contacts[I].Name);
if Pos(LowerSearch, LowerName) > 0 then
begin
WriteLn;
WriteLn(' [', I, '] ', Contacts[I].Name);
WriteLn(' Phone: ', Contacts[I].Phone);
WriteLn(' Email: ', Contacts[I].Email);
Found := True;
end;
end;
if not Found then
WriteLn('No contacts found matching "', SearchTerm, '".');
end;
Editing a Contact
This is where random access shines. We find the record, modify it in memory, and save the entire database (or, in a more advanced version, write just that record back to the file using Seek):
procedure EditContact;
var
SearchTerm: string;
Index: Integer;
NewValue: string;
begin
WriteLn;
Write('Edit — enter name to find: ');
ReadLn(SearchTerm);
Index := FindContact(SearchTerm);
if Index = -1 then
begin
WriteLn('Contact not found.');
Exit;
end;
WriteLn('Found: ', Contacts[Index].Name);
WriteLn(' Current phone: ', Contacts[Index].Phone);
WriteLn(' Current email: ', Contacts[Index].Email);
WriteLn;
Write('New phone (Enter to keep current): ');
ReadLn(NewValue);
if NewValue <> '' then
Contacts[Index].Phone := NewValue;
Write('New email (Enter to keep current): ');
ReadLn(NewValue);
if NewValue <> '' then
Contacts[Index].Email := NewValue;
WriteLn('Contact updated.');
end;
Deleting a Contact (Soft Delete)
procedure DeleteContact;
var
SearchTerm: string;
Index: Integer;
Confirm: Char;
begin
WriteLn;
Write('Delete — enter name to find: ');
ReadLn(SearchTerm);
Index := FindContact(SearchTerm);
if Index = -1 then
begin
WriteLn('Contact not found.');
Exit;
end;
WriteLn('Found: ', Contacts[Index].Name,
' | ', Contacts[Index].Phone,
' | ', Contacts[Index].Email);
Write('Delete this contact? (Y/N): ');
ReadLn(Confirm);
if UpCase(Confirm) = 'Y' then
begin
Contacts[Index].Deleted := True;
WriteLn('Contact deleted.');
end
else
WriteLn('Deletion cancelled.');
end;
Listing All Contacts
procedure ListContacts;
var
I: Integer;
ActiveCount: Integer;
begin
WriteLn;
WriteLn('--- All Contacts ---');
WriteLn(' # Name Phone Email');
WriteLn(' -- ---------------------------------------- --------------- --------------------------------------------------');
ActiveCount := 0;
for I := 0 to ContactCount - 1 do
begin
if Contacts[I].Deleted then Continue;
Inc(ActiveCount);
WriteLn(I:4, ' ',
Contacts[I].Name:-40, ' ',
Contacts[I].Phone:-15, ' ',
Contacts[I].Email);
end;
WriteLn;
WriteLn('Active contacts: ', ActiveCount);
WriteLn('Total records (including deleted): ', ContactCount);
end;
Compacting the Database
Over time, deleted records waste space. A "compact" operation removes them:
procedure CompactDatabase;
var
WritePos, ReadPos: Integer;
Removed: Integer;
begin
WritePos := 0;
Removed := 0;
for ReadPos := 0 to ContactCount - 1 do
begin
if not Contacts[ReadPos].Deleted then
begin
if WritePos <> ReadPos then
Contacts[WritePos] := Contacts[ReadPos];
Inc(WritePos);
end
else
Inc(Removed);
end;
ContactCount := WritePos;
WriteLn('Compacted database: removed ', Removed, ' deleted records.');
WriteLn('Active records: ', ContactCount);
end;
The Main Menu
var
Choice: Char;
begin
WriteLn('========================================');
WriteLn(' Phone Book — A Simple Contact Manager');
WriteLn('========================================');
WriteLn;
LoadDatabase;
repeat
WriteLn;
WriteLn('[A] Add contact');
WriteLn('[S] Search contacts');
WriteLn('[E] Edit contact');
WriteLn('[D] Delete contact');
WriteLn('[L] List all contacts');
WriteLn('[C] Compact database');
WriteLn('[Q] Save and quit');
Write('Choice: ');
ReadLn(Choice);
Choice := UpCase(Choice);
case Choice of
'A': AddContact;
'S': SearchContact;
'E': EditContact;
'D': DeleteContact;
'L': ListContacts;
'C': CompactDatabase;
'Q': SaveDatabase;
end;
until Choice = 'Q';
WriteLn('Goodbye!');
end.
Analysis: Design Decisions
Why Soft Deletes?
Physical deletion in a typed file requires either shifting all subsequent records forward (O(n) I/O operations) or moving the last record into the gap (changes ordering). Soft deletes avoid both problems at the cost of wasted space that can be reclaimed periodically through compaction.
In production databases, this pattern is extremely common. PostgreSQL, for instance, uses a similar "mark and vacuum" approach.
Why a Hybrid Memory/File Approach?
Loading everything into memory gives us fast in-session operations. Saving to a typed file gives us persistence. For a phone book of 500 contacts, the memory footprint is roughly 500 * SizeOf(TContact) bytes — well under 100 KB.
For a database of millions of records, this approach would not scale, and we would need to read/write records directly from the file using Seek. But for our tutoring business use case, it is perfectly adequate.
What Could Go Wrong?
-
Power failure during save: The file could be corrupted. A safer approach is to save to a temporary file, then rename it over the original — the
Renameoperation is atomic on most filesystems. -
Concurrent access: If two copies of the program run simultaneously, they will overwrite each other's changes. A production system would use file locking.
-
Record type changes: If we add a field to
TContact, old data files become incompatible. A versioning system (storing a version number as the first record) would help with migration.
Exercises Based on This Case Study
-
Add an "Export to CSV" feature that writes all active contacts to a text file in CSV format.
-
Add an "Import from CSV" feature that reads contacts from a CSV file and adds them to the database.
-
Implement the "safe save" pattern: write to
phonebook.tmp, then delete the oldphonebook.dat, then renamephonebook.tmptophonebook.dat. -
Add a
TContactV2record with aNotes: string[100]field. Write a migration procedure that reads a V1 file and creates a V2 file. -
Replace the linear search with a more efficient approach: maintain a sorted index array and use binary search. (Hint: sort by name, store indices into the main array.)