JustToThePoint English Website Version
JustToThePoint en español

Prayer Database Write Queries and Transactions

When you have eliminated the impossible, whatever remains, however improbable, must be the truth, Sherlock Holmes.

Image 

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.

The Write Method: runAsync

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

Insert a Prayer with Pages: Using Transactions

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, ...))

Partial Updates with Partial<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
}

Write Verification with changes

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;
}

Step 1 — Append write functions to src/db/prayerQueries.ts

We’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`);
}

Step 2 — Update app/(tabs)/prayers.tsx with the write test

Replace 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 },
});

Step 3 — Run and test

  1. Start (or restart) the Expo development server to ensure a fresh bundle: \
    npx expo start --clear
    
  2. Open the app on your device/simulator and navigate to the ✝ Prayers tab.
  3. Tap the ▶ Run write test butoon.

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.

Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.