sojorn/sojorn_docs/troubleshooting/search_function_debugging.md

9.4 KiB

Search Function Debugging Journey

Problem Summary

The search function was returning a 401 "Invalid JWT" error, then after deployment continued to return empty results despite the function being deployed.

Timeline of Issues and Fixes

Issue 1: Function Not Deployed

Problem: The search function existed in code but was never deployed to Supabase.

Error:

FunctionException(status: 401, details: {code: 401, message: Invalid JWT})

Root Cause: The function was missing from the deployment list in deploy_all_functions.ps1.

Fix:

  1. Added "search" to the $functions array in deploy_all_functions.ps1
  2. Deployed with: supabase functions deploy search --no-verify-jwt

Issue 2: Critical Code Bugs

After deployment, the function was returning 400 errors. Analysis revealed three critical bugs:

Bug 1: Performance - "Download the Internet" Bug

Problem: The tag search query had no limit and downloaded ALL posts into memory.

Bad Code:

const { data: tagData } = await serviceClient
  .from("posts")
  .select("tags")
  .not("tags", "is", null)
  .is("deleted_at", null);  // NO LIMIT!

Impact: Would timeout or crash with 1000+ posts in database.

Fix: Use a database view to aggregate tags at the database level:

const { data: tagsResult } = await serviceClient
  .from("view_searchable_tags")
  .select("tag, count")
  .ilike("tag", `%${safeQuery}%`)
  .order("count", { ascending: false })
  .limit(5);

View SQL:

CREATE OR REPLACE VIEW view_searchable_tags AS
SELECT
  unnest(tags) as tag,
  COUNT(*) as count
FROM posts
WHERE
  deleted_at IS NULL
  AND tags IS NOT NULL
  AND array_length(tags, 1) > 0
GROUP BY unnest(tags)
ORDER BY count DESC;

Bug 2: Syntax Error - Array Filter

Problem: PostgREST expects an array for in filters, not a formatted string.

Bad Code:

.not("id", "in", `(${excludeIds.join(",")})`)  // Wrong: passing string

Error: PGRST100 - PostgREST syntax error

Fix: Pass array with proper PostgREST string format:

if (excludeIds.length > 0) {
  dbQuery = dbQuery.not("id", "in", `(${excludeIds.join(",")})`);
}

Note: Must use the string format (val1,val2) for PostgREST, and check for empty array first.

Bug 3: Security - SQL Injection Risk

Problem: User input wasn't sanitized, allowing special characters to break PostgREST query syntax.

Bad Code:

const trimmedQuery = query.trim().toLowerCase();
// User searches "hello,world" -> breaks OR syntax

Impact: Commas and parentheses in user input caused 500 errors.

Fix: Sanitize query string:

const safeQuery = query.trim().toLowerCase().replace(/[,()]/g, "");

Issue 3: Wrong Column Name in blocks Table

Problem: Code referenced user_id column that doesn't exist.

Error:

ERROR: column blocks.user_id does not exist
SQL state code: 42703

Bad Code:

const { data: blockedUsers } = await serviceClient
  .from("blocks")
  .select("blocked_id")
  .eq("user_id", user.id);  // Wrong column name!

Actual Schema:

CREATE TABLE blocks (
    blocker_id UUID NOT NULL,
    blocked_id UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (blocker_id, blocked_id)
);

Fix:

const { data: blockedUsers } = await serviceClient
  .from("blocks")
  .select("blocked_id")
  .eq("blocker_id", user.id);  // Correct column name

Issue 4: Ambiguous Foreign Key Relationship

Problem: PostgREST couldn't determine which foreign key to use for the profiles join.

Error: PGRST201 - "Multiple objects found for foreign key"

Bad Code:

.select("id, body, created_at, author_id, author:profiles!inner(handle, display_name)")

Root Cause: The posts table has multiple foreign key relationships to profiles table, making the join ambiguous.

Fix: Explicitly specify the foreign key constraint name:

.select("id, body, created_at, author_id, profiles!posts_author_id_fkey(handle, display_name)")

Updated Processing Code:

const searchPosts: SearchPost[] = (postsResult.data || []).map((p: any) => ({
    id: p.id,
    body: p.body,
    author_id: p.author_id,
    author_handle: p.profiles?.handle || "unknown",
    author_display_name: p.profiles?.display_name || "Unknown User",
    created_at: p.created_at
}));

Additional Improvements

Parallel Query Execution

Used Promise.all() to run all three searches (users, tags, posts) simultaneously for better performance:

const [usersResult, tagsResult, postsResult] = await Promise.all([
  // User search
  (async () => { /* ... */ })(),

  // Tag search
  (async () => { /* ... */ })(),

  // Post search
  (async () => { /* ... */ })()
]);

This reduces total search time from sequential to parallel execution.


Final Working Code Structure

import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { createSupabaseClient, createServiceClient } from "../_shared/supabase-client.ts";

serve(async (req: Request) => {
  // 1. CORS handling
  if (req.method === "OPTIONS") { /* ... */ }

  try {
    // 2. Auth & Input Parsing
    const authHeader = req.headers.get("Authorization");
    if (!authHeader) throw new Error("Missing authorization header");

    let query = /* parse from POST body or query param */;
    if (!query || query.trim().length === 0) {
      return new Response(JSON.stringify({ users: [], tags: [], posts: [] }), {
        status: 200,
        headers: { "Content-Type": "application/json" }
      });
    }

    // Sanitize query
    const safeQuery = query.trim().toLowerCase().replace(/[,()]/g, "");

    // Verify auth
    const supabase = createSupabaseClient(authHeader);
    const { data: { user }, error: authError } = await supabase.auth.getUser();
    if (authError || !user) throw new Error("Unauthorized");

    // 3. Get blocked users list
    const { data: blockedUsers } = await serviceClient
      .from("blocks")
      .select("blocked_id")
      .eq("blocker_id", user.id);

    const excludeIds = (blockedUsers?.map(b => b.blocked_id) || []);
    excludeIds.push(user.id);

    // 4. Parallel search execution
    const [usersResult, tagsResult, postsResult] = await Promise.all([
      // Search users with proper exclusion
      // Search tags using view
      // Search posts with explicit FK reference
    ]);

    // 5-7. Process results
    // 8. Return JSON response

  } catch (error: any) {
    return new Response(
      JSON.stringify({ error: error.message || "Internal server error" }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    );
  }
});

Setup Requirements

1. Create Database View

The search function requires the view_searchable_tags view for efficient tag searching.

Location: supabase/migrations/create_searchable_tags_view.sql

Apply via Supabase Dashboard:

  1. Go to: https://supabase.com/dashboard/project/[YOUR_PROJECT_ID]/sql
  2. Run the SQL from the migration file

Verify:

SELECT * FROM view_searchable_tags LIMIT 10;

2. Deploy Function

supabase functions deploy search --no-verify-jwt

Or deploy all functions:

.\deploy_all_functions.ps1

Key Learnings

1. PostgREST Syntax is Strict

  • Array filters must use format: .not("id", "in", "(val1,val2)")
  • Empty arrays can cause errors - always check length first
  • Foreign key relationships must be explicit when ambiguous

2. Performance Matters

  • Never query unlimited rows from large tables
  • Use database views for aggregations
  • Use parallel queries (Promise.all()) when queries are independent

3. Security First

  • Always sanitize user input before using in queries
  • Remove special characters that can break query syntax
  • Characters to watch: , ( ) ' "

4. Schema Knowledge is Critical

  • Always verify actual column names in schema
  • Don't assume standard naming conventions
  • Use \d table_name in psql or check migration files

5. Explicit is Better Than Implicit

  • Specify foreign key constraint names when joining tables
  • Use service client for bypassing RLS
  • Check for edge cases (empty arrays, null values)

Debugging Checklist

When search returns no results:

  1. Check function deployment status

    supabase functions list | grep search
    
  2. Verify database has data

    • Test with broad search terms ("a", "the")
    • Check posts table has non-deleted records
  3. Review query logs

    • Check Supabase Dashboard > Logs
    • Look for 400/500 errors
    • Check PostgREST error codes
  4. Verify schema matches code

    • Column names correct?
    • Foreign key names correct?
    • Required views exist?
  5. Test queries directly in SQL

    • Run queries in Supabase SQL editor
    • Verify they return expected results
    • Check for RLS policy issues

Files Modified

  1. supabase/functions/search/index.ts - Complete rewrite
  2. deploy_all_functions.ps1 - Added search to deployment list
  3. supabase/migrations/create_searchable_tags_view.sql - New view
  4. supabase/CREATE_SEARCH_VIEW.md - Setup instructions

References