Component Queries

Component queries allow you to run SQL queries in your component code.

Component queries transform how we build data visualizations. Instead of passing data down through props from parent pages, components become self-contained units that can request exactly what they need. This independence makes components more reusable and easier to maintain, as all the logic for both fetching and displaying data lives in one place.

Static Queries

Static queries are "static" because the SQL string they run cannot change throughout the component's lifecycle. They are defined once when your component is created, and are executed when QueryLoad is mounted.

Here's how to create a component that fetches and displays information about tables in your database:

<script>
    import { buildQuery } from '@evidence-dev/component-utilities/buildQuery';
    import { QueryLoad } from '@evidence-dev/core-components';

    const query = buildQuery(
        'SELECT * FROM information_schema.tables',
    );
</script>

<QueryLoad data={query} let:loaded={tables}>
    <svelte:fragment slot="skeleton" />
    
    <ul>
        {#each tables as table}
            <li>{table.table_name}</li>
        {/each}
    </ul>
</QueryLoad>

The Query Loader

The QueryLoad component manages the entire lifecycle of your query execution. It handles:

  • Executing your query against DuckDB
  • Managing loading states
  • Handling any errors that occur
  • Delivering results to your component
<QueryLoad data={query} let:loaded={tableData}>
    <svelte:fragment slot="skeleton" />
    <!-- Your component content here -->
</QueryLoad>

The let:loaded directive creates a new variable containing your query results. Using a descriptive name (like tableData or salesMetrics) makes your code more maintainable than the generic loaded.

Dynamic Queries

For queries that change based on user input or component state, you need dynamic queries. This allows you to create interactive components.

Here's an example that lets users control how many rows to display:

<script>
    import { QueryLoad } from '@evidence-dev/core-components';
    import { getQueryFunction } from '@evidence-dev/component-utilities/buildQuery';
    import { Query } from '@evidence-dev/sdk/usql';

    // This will hold our current query result
    let query;

    // Create a reactive query function
    const queryFunction = Query.createReactive({
        execFn: getQueryFunction(),
        callback: v => query = v
    });

    // These values will control our query
    let limit = 10;
    let schemaName = 'public';

    // This reactive statement runs whenever limit or schemaName change
    $: queryFunction(`
        SELECT * 
        FROM information_schema.tables 
        WHERE table_schema = '${schemaName}'
        LIMIT ${limit}
    `);
</script>

<div>
    <label>
        Rows to show:
        <input type="number" bind:value={limit} min={0} />
    </label>
    <label>
        Schema:
        <input type="text" bind:value={schemaName} />
    </label>
</div>

<QueryLoad data={query} let:loaded={tables}>
    <svelte:fragment slot="skeleton" />
    <ul>
        {#each tables as table}
            <li>{table.table_name}</li>
        {/each}
    </ul>
</QueryLoad>

Let's understand how this works:

Query State Management

  1. First, we create a variable to hold our query result:

    let query;

    This will be updated every time our query executes with new parameters.

  2. Next, we create a reactive query function:

    const queryFunction = Query.createReactive({
        execFn: getQueryFunction(),
        callback: v => query = v
    });

    This sets up an environment that can execute queries and update our component's state.

  3. Finally, we use Svelte's reactive declarations to run our query:

    $: queryFunction(`SELECT * FROM ... LIMIT ${limit}`);

    The $: syntax tells Svelte to re-run this statement whenever limit changes, creating a connection between your component's state and your query.

Error Handling

When working with queries, things can sometimes go wrong. Maybe a query is malformed, or perhaps it's trying to access a table that doesn't exist. The QueryLoad component helps you handle these situations gracefully through its error slot:

<QueryLoad data={query} let:loaded={tables}>
    <svelte:fragment slot="skeleton" />
    
    <svelte:fragment slot="error" let:error>
        <div class="text-red-600">
            <h3 class="font-bold">Unable to load data</h3>
            <p>{error.message}</p>
            <p class="text-sm mt-2">
                Please check your query and try again.
            </p>
        </div>
    </svelte:fragment>

    <ul>
        {#each tables as table}
            <li>{table.table_name}</li>
        {/each}
    </ul>
</QueryLoad>

When a query fails, Evidence:

  1. Captures the error information
  2. Prevents the main content from rendering
  3. Makes the error details available through let:error
  4. Displays your error handling content