Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/corner-grocer/llms.txt

Use this file to discover all available pages before exploring further.

db_handler.py is the database access layer for Corner Grocer. It isolates every SQL statement and connection detail behind seven named functions so the rest of the application never constructs a query string or handles a raw cursor. This page explains when and why each function is called during a normal application run — for the full parameter-by-parameter API reference, see the db_handler reference.

How the module fits into the app

The application follows a clear division of responsibility: db_handler.py owns all database I/O, while corner_grocer_main.py owns the menu logic. CornerGrocerApp (defined in corner_grocer_main.py) calls the module’s functions in sequence at startup, during file ingestion, and in response to each menu selection.
from db_handler import (
    connect_to_db,
    create_table,
    insert_or_update_item,
    get_all_frequencies,
    get_frequency_for_item,
    get_frequencies_sorted_by_count,
    clear_all_data,
)
Notice that corner_grocer_main.py itself imports only the six functions it needs at runtime; clear_all_data is available for maintenance scripts or interactive resets but is not called from the main menu loop.

Startup sequence

When CornerGrocerApp.__init__ runs it calls two functions back to back:
def initialize_database(self):
    self.conn = connect_to_db(self.db_file)
    if not self.conn:
        raise Exception("Failed to connect to database")
    create_table(self.conn)
connect_to_db either opens the existing grocery.db file or lets SQLite create it from scratch — either way you get back a live sqlite3.Connection. create_table immediately follows and issues CREATE TABLE IF NOT EXISTS, so the schema is guaranteed to be in place before any reads or writes happen. On a first run both calls do real work; on every subsequent run connect_to_db just opens the file and create_table is a safe no-op.

Writing data: the upsert pattern

Every item that enters the database goes through insert_or_update_item. The function performs a manual upsert: it queries for an existing row, then either increments the stored frequency or inserts a fresh row with frequency = 1.
insert_or_update_item(conn, "apples")   # inserts: apples, frequency=1
insert_or_update_item(conn, "apples")   # updates: apples, frequency=2
insert_or_update_item(conn, "bananas")  # inserts: bananas, frequency=1
CornerGrocerApp.add_item strips whitespace and lowercases the item name before passing it here, so all stored keys are consistently lowercase. The function commits after every call, which means a crash mid-file leaves the database in a consistent (though incomplete) state rather than rolling back everything.

Reading data: three query patterns

Three retrieval functions cover the different views the menu needs: get_all_frequencies returns every item sorted A–Z. It backs menu options 2 (print list), 3 (print histogram), 4 (sort alphabetically), 6 (search), and 7 (save list to file).
frequencies = get_all_frequencies(conn)
for item, count in frequencies.items():
    print(f"{item}: {count}")
# apples: 2
# bananas: 1
# carrots: 5
get_frequency_for_item looks up one item by name and returns its count, or 0 if the item is not found. Menu option 1 uses this for the single-item lookup. The 0 sentinel means callers never have to check for None or catch an exception.
count = get_frequency_for_item(conn, "apples")
print(f"apples: {count}")        # apples: 2

missing = get_frequency_for_item(conn, "dragonfruit")
print(f"dragonfruit: {missing}") # dragonfruit: 0
get_frequencies_sorted_by_count orders items by descending frequency, breaking ties alphabetically. Menu options 5 (sort by frequency) and 8 (save histogram to file) use this to produce the ranked view.
ranked = get_frequencies_sorted_by_count(conn)
for item, count in ranked.items():
    print(f"{item}: {'*' * count}")
# carrots: *****
# apples:  **
# bananas: *

Resetting the database

Two approaches exist for clearing stored data, depending on whether you want to remove the file entirely or keep the schema in place. Delete the file — the cleanest reset; the next run recreates everything from scratch:
rm grocery.db
Clear rows programmatically — keeps the file and table structure, removes only the data:
from db_handler import connect_to_db, clear_all_data

conn = connect_to_db("grocery.db")
clear_all_data(conn)
conn.close()
clear_all_data executes DELETE FROM grocery_frequency and commits. The table structure is preserved and connect_to_db / create_table will not need to be called again before the next write — the table already exists, just empty.
clear_all_data is irreversible. Once committed, deleted rows cannot be recovered unless you have a backup copy of grocery.db. Copy the .db file to a safe location before calling this function if you may need the data later.

Error handling philosophy

Every function in db_handler.py catches sqlite3.Error internally and either returns a safe fallback value (None, 0, or {}) or calls sys.exit(1) for unrecoverable failures. The table below summarises the failure behaviour:
FunctionOn error
connect_to_dbReturns None; caller must check before proceeding
create_tablePrints error and calls sys.exit(1) — cannot continue without a valid schema
insert_or_update_itemPrints error and returns; the failed item is silently skipped
get_all_frequenciesReturns {}
get_frequency_for_itemReturns 0
get_frequencies_sorted_by_countReturns {}
clear_all_dataPrints error and returns without committing
The asymmetry between create_table (which exits) and the read/write functions (which return fallbacks) is intentional: if the schema cannot be created the application has no usable state, whereas a failed read or skipped write is a degraded-but-recoverable condition.

Build docs developers (and LLMs) love