When you have eliminated the impossible, whatever remains, however improbable, must be the truth, Sherlock Holmes.
This section covers every pattern you need to safely create, update, and delete rows using expo-sqlite, with full TypeScript type safety, atomic transactions, and robust error handling. All examples assume you have a db instance exported from your database setup and the table schemas defined in earlier chunks.
Every statement that modifies data (INSERT, UPDATE, DELETE) is executed via db.runAsync(). It returns a SQLiteRunResult object:
const result = await db.runAsync(
"INSERT INTO myprayers (title) VALUES (?)",
["Morning Prayer"]
);
/* Returned Properties */
result.lastInsertRowId
/* The rowid of the most recent successful INSERT on this database connection.
This is how you retrieve the auto-incremented primary key after adding a row.
After inserting a prayer, you need its id_myprayer to insert its pages. */
result.changes
/* The number of rows affected by the statement.
For an INSERT, it's typically 1; for UPDATE or DELETE it tells you how many rows matched the WHERE clause.
When you update or delete, you must confirm something actually happened → check result.changes > 0 */
Equivalent Python:
cursor = conn.execute("INSERT INTO myprayers (title) VALUES (?)", ("Morning Prayer",))
cursor.lastrowid # same as lastInsertRowId
cursor.rowcount # same as changes
When you create a prayer, you typically also insert its pages immediately. These two steps must be atomic: if the pages insertion fails, the prayer row should not exist alone. A transaction groups multiple statements into a single unit of work that either fully succeeds or is completely rolled back.
await db.withTransactionAsync(async () => {
// All operations inside this callback are part of the transaction
const result = await db.runAsync("INSERT INTO myprayers ...", [...]);
const prayerId = result.lastInsertRowId;
for (const page of prayer.pages) {
await db.runAsync(
'INSERT INTO pages (id_myprayer, page_number, content) VALUES (?, ?, ?)',
[prayerId, page.page_number, page.content]
);
}
// If anything throws here, BOTH inserts are rolled back
});
Equivalent Python:
with conn: # the context manager automatically commits/rolls back
cursor = conn.execute("INSERT INTO myprayers ...", (...))
prayer_id = cursor.lastrowid
for page in pages:
conn.execute("INSERT INTO pages ...", (prayer_id, ...))
<T>When you update a prayer, the user may change only the title, only the text, or both. You don’t want to require all fields. TypeScript’s Partial<T> makes all properties optional:
type UpdatePrayerInput = Partial<{
title: string;
prayer_text: string;
updated_at: string;
}>;
async function updatePrayer(id: number, updates: UpdatePrayerInput) {
// Only update the fields that are provided
}
After every UPDATE or DELETE, check result.changes:
const result = await db.runAsync(
'UPDATE myprayers SET title = ? WHERE id_myprayer = ?',
['New Title', id]
);
/* This ensures you don't silently fail when the row doesn't exist – instead you throw a meaningful error. */
if (result.changes === 0) {
throw new Error(`Prayer with id ${id} not found`);
}
/* For deletes, you can return a boolean indicating whether a row was removed: */
async function deletePrayer(id: number): Promise<boolean> {
const result = await db.runAsync(
'DELETE FROM myprayers WHERE id_myprayer = ?',
[id]
);
return result.changes > 0;
}
src/db/prayerQueries.tsWe’re adding the write functions to the prayer database layer. They’re appended at the end of src/db/prayerQueries.ts, right after the read queries from our previous article. This keeps all prayer database operations in one file, making imports simple and maintenance straightforward.
// ──────────────────────────────────────────────────────────────────────────────
// WRITE QUERIES
// ──────────────────────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════════
// ADD PRAYER
// ══════════════════════════════════════════════════════════════════════════════
/**
* Insert a new prayer row into the myprayers table, and return its id_myprayer.
*
*
* @param db Open database connection
* @param title Required. Prayer display name. No default because every prayer must have a title.
* @param description One-sentence summary; optional, defaults to an empty string.
* @param date ISO date "YYYY-MM-DD". optional, defaults to today’s date in ISO format (YYYY-MM-DD).
* @param image Optional. It can be null (no image) or a base64 string.
* The type string | null explicitly tells TypeScript that null is a valid value, distinct from an empty string.
* @returns The return type is Promise<number> — the new row’s primary key (id_myprayer)
* @throws Error if title is empty.
*
* @example
* const id = await addPrayer(db, "Evening Reflection",
* "A quiet review of the day.", "2026-05-14");
*/
export async function addPrayer(
db: SQLiteDatabase,
title: string,
description: string = "",
date: string = todayISO(),
image: string | null = null
): Promise<number> {
// Validation. We enforce that the title isn’t empty or just whitespace.
// This catches accidental blank entries from the UI.
const trimmedTitle = title.trim();
if (!trimmedTitle) {
throw new Error("Prayer title cannot be empty.");
}
// The INSERT Statement
// We use positional parameters (?) to prevent SQL injection. Expo‑SQLite’s runAsync accepts an array of values.
// A || B returns: (1) A, if A is truthy; or (2) B, if A is falsy: "", 0, null, undefined, or false
// If date is an empty string after trimming, we store NULL instead.
// This ensures that “no date” is represented consistently in the database. It also enables WHERE date IS NULL queries (for undated prayers) to work correctly.
// image is passed as‑is; null means no image.
const result = await db.runAsync(
`INSERT INTO myprayers (title, description, date, image)
VALUES (?, ?, ?, ?)`,
[trimmedTitle, description.trim(), date.trim() || null, image]
);
// Retrieving the New Primary Key
// After any successful INSERT, lastInsertRowId holds the auto‑generated id_myprayer.
// const result = await db.runAsync(...); and the Expo SQLite typings define runAsync like this:
/* runAsync(): Promise<{
lastInsertRowId: number;
rowsAffected: number;
}>
*/
// TypeScript knows this is a number because SQLiteRunResult types it as such.
const newId = result.lastInsertRowId;
// A simple log for development.
console.log(`[DB] addPrayer: inserted id=${newId} title="${trimmedTitle}"`);
return newId;
}
// ══════════════════════════════════════════════════════════════════════════════
// UPDATE PRAYER
// ══════════════════════════════════════════════════════════════════════════════
/**
* Modify one or more fields of an existing prayer.
* It uses the partial update pattern: you pass an object containing only the fields you want to change.
* All other columns keep their current values
*
* Partial<T> is a built‑in TypeScript utility type that makes every property optional.
* Here, T is the type of a new prayer’s fields (NewPrayer).
* So fields becomes an object where you can provide title?, description?, date, and image.
* NewPrayer = { title, description, date, image }
* Partial<NewPrayer> = { title?, description?, date?, image? }
* (all fields become optional)
*
* @param db Open database connection
* @param id id_myprayer of the prayer to update
* @param fields Object with the fields to change
* @returns true if a row was updated, false if id was not found
*
* @example
* // Update only the title
* await updatePrayer(db, 1, { title: "Evening Examen" });
*
* // Update title and date together
* await updatePrayer(db, 1, { title: "New Title", date: "2026-05-15" });
*/
export async function updatePrayer(
db: SQLiteDatabase,
id: number,
fields: Partial<{
title: string;
description: string;
date: string;
image: string | null;
}>
): Promise<boolean> {
// Build the SET clause dynamically from the fields object.
// Only include fields that were actually passed.
//
// Example: fields = { title: "New", date: "2026-05-14" }
// sets = ["title = ?", "date = ?"]
// values = ["New", "2026-05-14"]
const sets: string[] = [];
const values: (string | number | null)[] = [];
// For each property that is not undefined:
// We add the column name to sets (e.g. "title = ?").
// We push the (trimmed) value into values.
// undefined means the caller didn’t pass that field at all → ignore it.
// null means the caller explicitly wants to set the field to NULL.
if (fields.title !== undefined) {
sets.push("title = ?");
values.push(fields.title.trim());
}
if (fields.description !== undefined) {
sets.push("description = ?");
values.push(fields.description.trim());
}
if (fields.date !== undefined) {
sets.push("date = ?");
values.push(fields.date.trim() || null);
}
if (fields.image !== undefined) {
sets.push("image = ?");
values.push(fields.image);
}
// Handling an Empty Update: If no fields were provided (an empty object), sets.length is 0.
// We early‑return false and log a warning.
if (sets.length === 0) {
console.warn("[DB] updatePrayer: no fields to update.");
return false;
}
// Append the WHERE clause value at the end (id_myprayer of the prayer to update)
values.push(id);
// Executing the UPDATE
// The values array already contains the id at the end (appended after the field values).
// This keeps the parameter ordering simple: all field placeholders come first, then the WHERE clause parameter.
const sql = `UPDATE myprayers SET ${sets.join(", ")} WHERE id_myprayer = ?`;
const result = await db.runAsync(sql, values);
// Interpreting changes. changes tells us how many rows were actually modified.
// If the prayer with the given id exists, it’s 1. If no such row exists, it’s 0.
// We return a boolean so the caller can distinguish between “update succeeded” and “prayer not found”.
const updated = result.changes > 0;
// Logging, simple console tracing for debugging purposes.
console.log(`[DB] updatePrayer id=${id}: ${updated ? "updated" : "not found"}`);
return updated;
}
// ══════════════════════════════════════════════════════════════════════════════
// REMOVE PRAYER
// ══════════════════════════════════════════════════════════════════════════════
/**
* Delete a prayer and (cascading) all its pages.
*
* WHY pages are deleted automatically:
* The pages table was defined with:
* REFERENCES myprayers(id_myprayer) ON DELETE CASCADE
* When we delete a myprayers row, SQLite automatically deletes all
* pages rows where myprayerid matches — we do not need a second DELETE.
* Important prerequisite: Foreign key enforcement must be turned on.
* In initDatabase, you should have executed: PRAGMA foreign_keys = ON;
*
* @param db Open database connection
* @param id id_myprayer to delete
* @returns true if the prayer was found and deleted, false if not found
*/
export async function removePrayer(
db: SQLiteDatabase,
id: number
): Promise<boolean> {
// The SQL statement deletes the prayer with the given id.
// Thanks to the foreign key definition on the pages table:
// myprayerid INTEGER NOT NULL REFERENCES myprayers(id_myprayer) ON DELETE CASCADE
// when we delete a row from myprayers, SQLite automatically deletes all child rows in pages that reference it.
// This is called a cascade delete.
const result = await db.runAsync(
"DELETE FROM myprayers WHERE id_myprayer = ?",
[id]
);
// result.changes tells us how many rows were deleted from myprayers.
// It will be 1 if a prayer with that id existed, 0 if not
const deleted = result.changes > 0;
console.log(`[DB] removePrayer id=${id}: ${deleted ? "deleted" : "not found"}`);
// Error Handling. If the DELETE fails due to a database corruption or disk error, runAsync will throw.
// We let that bubble up — the caller should wrap the call in a try/catch.
return deleted;
}
// ══════════════════════════════════════════════════════════════════════════════
// ADD PAGE
// ══════════════════════════════════════════════════════════════════════════════
/**
* Insert or Replace a Single Page
* If a page with the same page_number already exists for this prayer,
* it is replaced (INSERT OR REPLACE semantics).
*
* @param db Open database connection
* @param prayerId id_myprayer of the parent prayer (must exist)
* @param pageNumber 1-based position within the prayer
* @param content HTML or plain text for this prayer step
* @param image Optional base64-encoded per-page image
* @returns The new (or replaced) page's id
* @throws Error if prayerId does not exist in myprayers
*
* @example
* await addPage(db, prayerId, 1, "<h2>Presence</h2><p>…</p>");
* await addPage(db, prayerId, 2, "<h2>Freedom</h2><p>…</p>");
*/
export async function addPage(
db: SQLiteDatabase,
prayerId: number,
pageNumber: number,
content: string = "",
image: string | null = null
): Promise<number> {
// Verify the parent prayer exists before inserting.
// Without this check, inserting a page for a non-existent prayer
// would fail with a less helpful foreign key constraint error.
const parent = await db.getFirstAsync(
"SELECT id_myprayer FROM myprayers WHERE id_myprayer = ?",
[prayerId]
);
if (parent === null) {
throw new Error(
`Cannot add page: prayer with id=${prayerId} does not exist.`
);
}
// INSERT OR REPLACE is SQLite’s conflict‑resolution algorithm.
// For it to work, the "page" table must have a unique constraint that can cause a conflict: UNIQUE(myprayerid, page_number)
// If (myprayerid, page_number) is unique → inserts a new row. This function inserts a single page for a given prayer.
// If it already exists → deletes the old row and inserts the new one. It updates an existing prayer.
// When you try to insert a row that violates this unique constraint, instead of throwing an error, SQLite deletes the old row and inserts the new one.
const result = await db.runAsync(
`INSERT OR REPLACE INTO pages (myprayerid, page_number, content, image)
VALUES (?, ?, ?, ?)`,
[prayerId, pageNumber, content, image]
);
// result.lastInsertRowId gives the id of the newly inserted (or replaced) page
const newId = result.lastInsertRowId;
console.log(
`[DB] addPage: prayer=${prayerId} page=${pageNumber} id=${newId}`
);
return newId;
}
// ══════════════════════════════════════════════════════════════════════════════
// ADD CONTENT PRAYER (bulk insert pages)
// ══════════════════════════════════════════════════════════════════════════════
/**
* Bulk-add multiple pages to a prayer inside a single transaction.
*
* This is the primary way to populate a brand‑new prayer with its pages immediately after creating it.
* Instead of calling addPage multiple times (which would be several separate transactions),
* we insert all pages inside a single transaction. This guarantees all‑or‑nothing behaviour.
*
* @param db Open database connection
* @param prayerId id_myprayer of the target (parent) prayer, assumed to already exist.
* @param pages Array of page objects, each with:
* page_number (number, required)
* content (string, required)
* image (string | null, optional)
* @returns Array of inserted page ids (in input order)
*
* @example
* const ids = await addContentPrayer(db, prayerId, [
* { page_number: 1, content: "</h2>Awareness</h2></p>…</p>" },
* { page_number: 2, content: "</h2>Gratitude</h2></p>…</p>" },
* { page_number: 3, content: "</h2>Petition</h2></p>…</p>" },
* ]);
* console.log(ids); // [4, 5, 6] ← the new page ids
*/
export async function addContentPrayer(
db: SQLiteDatabase,
prayerId: number,
pages: Array<{
page_number: number;
content: string;
image?: string | null;
}>
): Promise<number[]> {
const ids: number[] = [];
// All page insertions happen inside the callback passed to withTransactionAsync.
// If any INSERT throws an error (e.g. database corruption), the entire transaction is rolled back and no pages are committed.
// This prevents a half‑populated prayer.
await db.withTransactionAsync(async () => {
// Inside the transaction, we iterate through the pages array ...
for (const page of pages) {
// and perform the same INSERT OR REPLACE as in addPage
// Because we are already inside a transaction, each individual runAsync doesn’t commit;
// the changes are only made permanent when the callback finishes successfully.
const result = await db.runAsync(
`INSERT OR REPLACE INTO pages (myprayerid, page_number, content, image)
VALUES (?, ?, ?, ?)`,
[prayerId, page.page_number, page.content, page.image ?? null]
);
// We push result.lastInsertRowId into an array for each successful insert.
// Since we get a new id for each page, we can return the list in insertion order.
ids.push(result.lastInsertRowId);
}
});
// NOTE: We assume the prayer exists because this function is typically called right after addPrayer (which returns a valid id).
// If the prayer doesn’t exist, the INSERT OR REPLACE will fail with a foreign key constraint violation,
// and the transaction will roll back. This is acceptable for a bulk helper
console.log(
// A single log line reports how many pages were inserted and their ids, which helps during development.
`[DB] addContentPrayer: prayer=${prayerId} ` +
`inserted ${ids.length} pages [${ids.join(", ")}]`
);
return ids;
}
// ══════════════════════════════════════════════════════════════════════════════
// REMOVE PAGE
// ══════════════════════════════════════════════════════════════════════════════
/**
* Simple and straightforward: it deletes a page directly by its primary key (id).
* It returns true if a page was deleted, false if the page didn’t exist.
* There are no child tables referencing pages, so we don’t worry about orphaned data. The page is simply gone.
*
* @param db Open database connection
* @param pageId The pages.id to delete
* @returns true if deleted, false if not found
*/
export async function removePage(
db: SQLiteDatabase,
pageId: number
): Promise<boolean> {
const result = await db.runAsync(
"DELETE FROM pages WHERE id = ?",
[pageId]
);
const deleted = result.changes > 0;
console.log(`[DB] removePage id=${pageId}: ${deleted ? "deleted" : "not found"}`);
return deleted;
}
// ══════════════════════════════════════════════════════════════════════════════
// REORDER PAGES
// ══════════════════════════════════════════════════════════════════════════════
/**
* Reassign page_number values for a prayer's pages.
* Used when the user reorders pages by drag-and-drop.
*
* @param db Open database connection
* @param prayerId id_myprayer of the prayer
* @param pageIds Array of page IDs (pages.id) in the desired order.
* The first element gets page_number=1, the second page 2, etc.
*
* @example
* After this, page 12 becomes page_number 1, page 10 becomes 2, and page 11 becomes 3.
* await reorderPages(db, prayerId, [12, 13, 11]);
*/
export async function reorderPages(
db: SQLiteDatabase,
prayerId: number,
pageIds: number[]
): Promise<void> {
// The entire loop is wrapped in withTransactionAsync so that if any update fails, all changes are rolled back.
// This keeps the page numbering consistent.
await db.withTransactionAsync(async () => {
for (let i = 0; i < pageIds.length; i++) {
// i is 0-based; page_number is 1-based
await db.runAsync(
`UPDATE pages SET page_number = ?
WHERE id = ? AND myprayerid = ?`,
[i + 1, pageIds[i], prayerId]
);
}
});
console.log(`[DB] reorderPages: prayer=${prayerId} reordered ${pageIds.length} pages`);
}
app/(tabs)/prayers.tsx with the write testReplace the entire file with the following. This screen is a self‑contained, idempotent test harness that exercises every write operation in the prayer database layer, one after another. It uses the SQLite context from Expo and the query functions you built earlier. The screen can be run repeatedly without affecting the seed data created previously.
// app/(tabs)/prayers.tsx
//
// PRAYERS TAB — Chunk 08 test screen
//
// Tests all write operations in sequence:
// 1. addPrayer + addContentPrayer → Create a new prayer + Inserts 3 pages linked to the prayer
// 2. getPrayerById → Fetches the prayer including its pages; confirms 3 pages are present
// 3. updatePrayer → Changes the prayer title to "Evening Examen"; returns true on success
// 4. getPrayerById again → Re‑reads the prayer and asserts that the title has actually changed
// 5. removePage → Deletes a single page (page 2) by its id; returns true
// 6. getPrayerById again → Confirms that the prayer now has only 2 pages after the deletion
// 7. removePrayer → Deletes the entire prayer (and, via CASCADE, all remaining pages); returns true
// 8. getPrayerById again → Verifies that the prayer no longer exists — null is returned (no rows)
//
import React, { useState } from "react";
import {
View, Text, ScrollView, StyleSheet,
TouchableOpacity, ActivityIndicator,
} from "react-native";
import { useSQLiteContext } from "expo-sqlite";
import {
addPrayer, updatePrayer, removePrayer,
addPage, addContentPrayer, removePage,
getPrayerById,
} from "../../src/db/prayerQueries";
import type { Prayer } from "../../src/db/types";
import { todayISO } from "../../src/utils/dateUtils";
// ── Log entry type ─────────────────────────────────────────────────────────────
interface LogEntry {
step: number;
label: string;
ok: boolean;
detail: string;
}
// State management
export default function PrayersScreen() {
const db = useSQLiteContext();
/* running – disables the button while the async test is in progress, shows a spinner. */
const [running, setRunning] = useState(false);
/* logs – an array of LogEntry objects (step, label, ok, detail) that feed the visual log. */
const [logs, setLogs] = useState<LogEntry[]>([]);
/* done – flips to true once the test finishes (success or failure), so the summary appears. */
const [done, setDone] = useState(false);
// ── Test runner ──────────────────────────────────────────────────────────────
async function runWriteTest() {
/* Immediately resets the UI, sets running = true and done = false. */
setRunning(true);
setDone(false);
const entries: LogEntry[] = [];
/* A helper log() function pushes an entry to the local entries array and then calls setLogs([...entries]).
Spreading the array creates a new reference, which tells React to re‑render with the latest log.
This makes the UI update after each step, giving the feel of a live test. */
function log(step: number, label: string, ok: boolean, detail: string) {
entries.push({ step, label, ok, detail });
// Spread to create a new array reference — React re-renders on reference change
setLogs([...entries]);
}
let prayerId = -1;
let prayer: Prayer | null = null;
let page2Id: number = -1;
/* The steps proceed sequentially inside a try block.
Any thrown error is caught, logged as an “Unhandled error” step, and the test stops gracefully. */
try {
// ── Step 1: addPrayer ────────────────────────────────────────────────────
prayerId = await addPrayer(
db,
"Evening Reflection",
"A quiet review of the day.",
todayISO()
);
log(1, "addPrayer()", prayerId > 0,
`id = ${prayerId}`);
// ── Step 2: addContentPrayer (3 pages) ───────────────────────────────────
const pageIds = await addContentPrayer(db, prayerId, [
{ page_number: 1, content: "<h2>Awareness</h2><p>I pause and become aware of God's presence.</p>" },
{ page_number: 2, content: "<h2>Gratitude<</h2><p>What am I grateful for today?</p>" },
{ page_number: 3, content: "<h2&lgt;Petition</h2><p>What do I need to ask for tomorrow?</p>" },
]);
log(2, "addContentPrayer() 3 pages", pageIds.length === 3,
`page ids = [${pageIds.join(", ")}]`);
page2Id = pageIds[1]; // save page 2's id for the removePage test
// ── Step 3: getPrayerById to verify ────────────────────────────────────
prayer = await getPrayerById(db, prayerId);
const step3ok = prayer !== null && prayer.pages.length === 3;
log(3, "getPrayerById() — verify 3 pages", step3ok,
prayer
? `title="${prayer.title}" pages=${prayer.pages.length}`
: "returned null");
// ── Step 4: updatePrayer ────────────────────────────────────────────────
const updated = await updatePrayer(db, prayerId, {
title: "Evening Examen",
});
log(4, "updatePrayer() — change title", updated,
`changed=${updated}`);
// ── Step 5: getPrayerById again — confirm title changed ─────────────────
prayer = await getPrayerById(db, prayerId);
const step5ok = prayer?.title === "Evening Examen";
log(5, "getPrayerById() — confirm new title", step5ok,
`title="${prayer?.title}"`);
// ── Step 6: removePage — delete page 2 ─────────────────────────────────
const pageRemoved = await removePage(db, page2Id);
log(6, "removePage() — delete page 2", pageRemoved,
`deleted=${pageRemoved} (page id=${page2Id})`);
// ── Step 7: getPrayerById — confirm 2 pages remain ─────────────────────
prayer = await getPrayerById(db, prayerId);
const step7ok = prayer?.pages.length === 2;
log(7, "getPrayerById() — confirm 2 pages", step7ok,
`pages=${prayer?.pages.length}`);
// ── Step 8: removePrayer ────────────────────────────────────────────────
/* removePrayer relies on CASCADE deletes in the database schema, so removing a prayer automatically deletes its pages. */
const deleted = await removePrayer(db, prayerId);
log(8, "removePrayer()", deleted, `deleted=${deleted}`);
// ── Step 9: getPrayerById — confirm null ────────────────────────────────
const gone = await getPrayerById(db, prayerId);
log(9, "getPrayerById() — confirm null (cascade)", gone === null,
`result=${JSON.stringify(gone)}`);
} catch (e) {
// If any step throws an unhandled error, log it clearly
entries.push({
step: entries.length + 1,
label: "Unhandled error",
ok: false,
detail: String(e),
});
setLogs([...entries]);
}
/* The test always ends with setRunning(false) and setDone(true). */
setRunning(false);
setDone(true);
}
// ── Render ───────────────────────────────────────────────────────────────────
/* done means the test sequence has finished (success or failure).
logs.every((l) => l.ok) checks whether every log entry has ok === true.
If all steps passed → true. If any step failed → false.
In summary, All tests passed only if the test is finished AND every step succeeded. */
const allPassed = done && logs.every((l) => l.ok);
return (
/* This component renders a test screen that:
1. Shows a heading and description. */
<ScrollView style={s.scroll} contentContainerStyle={s.content}>
<Text style={s.heading}>Write Queries Test</Text>
<Text style={s.sub}>
Tests addPrayer, addContentPrayer, updatePrayer,{"\n"}
removePage, and removePrayer in sequence.
</Text>
/* 2. Shows a button that runs a sequence of async database write tests. */
<TouchableOpacity
/* If running === true, running && s.btnDisabled → s.btnDisabled. Final style becomes: [s.btn, s.btnDisabled]
If running === false, running && s.btnDisabled → false. Final style becomes: [s.btn, false].
React ignores false, so only s.btn is applied. */
/* When running === true: (iii) The style adds s.btnDisabled */
style={[s.btn, running && s.btnDisabled]}
onPress={runWriteTest}
/* When running === true: (i) The button is disabled */
disabled={running}
>
/* When running === true: (ii) The button content switches to: ActivityIndicator (a spinner) */
{running ? (
<ActivityIndicator color="#ffffff" size="small" />
) : (
/* When running === false: The button is clickable again, and the text depends on done:
Meaning: Before the first run → “▶ Run write test”. After finishing → “↺ Run again” */
<Text style={s.btnText}>
{done ? "↺ Run again" : "▶ Run write test"}
</Text>
)}
</TouchableOpacity>
{/* Log entries. As each step finishes, a log entry appears. Recall log function: entries.push({ step, label, ok, detail }); */}
{logs.map((entry) => (
<View
key={entry.step}
style={[s.logRow, entry.ok ? s.logOk : s.logFail]}
>
/* Each log row shows: A ✅ or ❌ indicator; The step number and label;
A human‑readable detail string (entry.detail, i.e., the returned id, title, page count, etc.). */
<View style={s.logHeader}>
<Text style={s.logStep}>
{entry.ok ? "✅" : "❌"} Step {entry.step}
</Text>
<Text style={[s.logLabel, entry.ok ? s.textOk : s.textFail]}>
{entry.label}
</Text>
</View>
<Text style={s.logDetail}>{entry.detail}</Text>
</View>
))}
{/* When all steps finish: a summary box appears; if all steps passed, it shows a success message;
the button changes to “Run again” */}
{done && (
<View style={[s.summary, allPassed ? s.summaryOk : s.summaryFail]}>
<Text style={allPassed ? s.textOk : s.textFail}>
{allPassed
? "✅ All 9 steps passed — write queries working correctly."
: "❌ Some steps failed — check the Metro log for details."}
</Text>
/* In JavaScript: true && something → returns something; false && something → returns false (React renders nothing)
Meaning: “Only show this message if all tests passed.” */
{allPassed && (
<Text style={s.summaryHint}>
{"\n"}New code complete.{"\n"}
Next: Database Seed on First Launch.
</Text>
)}
</View>
)}
</ScrollView>
);
}
const s = StyleSheet.create({
scroll: { flex: 1, backgroundColor: "#1a1f2e" },
content: { padding: 20, paddingBottom: 48 },
heading: { fontSize: 18, fontWeight: "bold", color: "#c9a84c",
marginBottom: 4 },
sub: { fontSize: 12, color: "#6b7280", marginBottom: 20,
lineHeight: 18 },
btn: { backgroundColor: "#5b8fd4", borderRadius: 8, padding: 14,
alignItems: "center", marginBottom: 20 },
btnDisabled: { backgroundColor: "#2d3550" },
btnText: { color: "#ffffff", fontSize: 15, fontWeight: "bold" },
logRow: { borderRadius: 8, padding: 12, marginBottom: 8,
borderWidth: 1 },
logOk: { backgroundColor: "#1a2e1a", borderColor: "#2d4a2d" },
logFail: { backgroundColor: "#2e1a1a", borderColor: "#4a2d2d" },
logHeader: { flexDirection: "row", alignItems: "center",
gap: 8, marginBottom: 4 },
logStep: { fontSize: 13, color: "#dde4f0", fontWeight: "600" },
logLabel: { fontSize: 13, fontWeight: "600", flex: 1 },
logDetail: { fontSize: 11, color: "#6b7280", fontFamily: "monospace",
lineHeight: 16 },
textOk: { color: "#86efac" },
textFail: { color: "#f87171" },
summary: { borderRadius: 10, padding: 16, marginTop: 8,
borderWidth: 1 },
summaryOk: { backgroundColor: "#1a2e1a", borderColor: "#2d4a2d" },
summaryFail: { backgroundColor: "#2e1a1a", borderColor: "#4a2d2d" },
summaryHint: { fontSize: 12, color: "#4b5563", fontFamily: "monospace",
lineHeight: 18 },
});
npx expo start --clear
You should see the nine steps appear one after another, each with a ✅, followed by a green summary:
✅ Step 1 addPrayer() id = 2
✅ Step 2 addContentPrayer() 3 pages page ids = [6, 7, 8]
✅ Step 3 getPrayerById() — verify 3 pages title="Evening Reflection" pages=3
✅ Step 4 updatePrayer() — change title changed=true
✅ Step 5 getPrayerById() — confirm title title="Evening Examen"
✅ Step 6 removePage() — delete page 2 deleted=true (page id=7)
✅ Step 7 getPrayerById() — confirm 2 pages pages=2
✅ Step 8 removePrayer() deleted=true
✅ Step 9 getPrayerById() — confirm null result=null
✅ All 9 steps passed — write queries working correctly.
Tap ↺ Run again. A new prayer with a new id is created and then deleted. The seed prayer remains untouched, proving the test’s idempotency and the correctness of your write operations.