Skip to content

Feature Request: Lazy evaluation for sqlpage.fetch_with_meta() to support conditional HTTP requests #1337

Description

@amrutadotorg

Summary

Hi @lovasoa , when building a cache layer for external API calls, we need sqlpage.fetch_with_meta() to execute only when the cache is empty. Unfortunately, SQLite does not guarantee short-circuit evaluation for side-effect functions, so the fetch is executed every time regardless of the condition.

Problem

We want this pattern:

SET cached = (SELECT data FROM cache WHERE ...);
-- Only fetch if cache is empty
SET response = CASE WHEN $cached IS NULL THEN sqlpage.fetch_with_meta(...) END;

But sqlpage.fetch_with_meta() is always called, even when $cached IS NOT NULL.

What we tested

Test 1: CASE WHEN — ❌ Does NOT block evaluation

SET cached = (SELECT data FROM cache WHERE updated_at > datetime('now', '-1 hour'));

SET response = CASE
    WHEN $cached IS NULL
    THEN sqlpage.fetch_with_meta(json_object('method', 'GET', 'url', 'https://api.example.com/data'))
END;

Result: sqlpage.fetch_with_meta() is called EVERY time, even when $cached IS NOT NULL.

Log evidence:

Fetching https://api.example.com/data with metadata   ← fetch executed!
[API] cached=HIT, response=NULL                       ← but cache was HIT

SQLite's CASE expression does not guarantee short-circuit evaluation for functions with side effects.

Test 2: Subquery with WHERE — ❌ Does NOT block evaluation

SET response = (
    SELECT sqlpage.fetch_with_meta(json_object('method', 'GET', 'url', 'https://api.example.com/data'))
    FROM (SELECT 1) AS _dummy
    WHERE $cached IS NULL
);

Result: sqlpage.fetch_with_meta() is STILL called, even when WHERE would return 0 rows.

Log evidence:

Fetching https://api.example.com/data with metadata   ← fetch executed!
[API] cached=HIT, response=NULL                       ← but cache was HIT

SQLite's query planner evaluates scalar functions in SELECT regardless of the WHERE clause.

Test 3: localhost fallback — ✅ Works (but is a hack)

SET should_fetch = CASE WHEN $cached IS NULL THEN 1 ELSE 0 END;

SET request = CASE WHEN $should_fetch = 1 THEN
    json_object('method', 'GET', 'url', 'https://api.example.com/data')
ELSE
    json_object('method', 'HEAD', 'url', 'http://127.0.0.1:8080/style.css')
END;

SET response = sqlpage.fetch_with_meta($request);

Result: Works! When cache is HIT, the request goes to localhost instead of the external API. No external transfer, no API quota used.

Log evidence:

Fetching http://127.0.0.1:8080/style.css with metadata   ← localhost ping
[API] should_fetch=0, status=200 — cache HIT             ← no external call

But this is a hack — it makes 4 unnecessary localhost HTTP requests per page load (one per API: Google, Open-Meteo, IMGW, IAU).

Why this matters

  • The localhost hack adds ~4 unnecessary HTTP round-trips per page load
  • It pollutes logs with Fetching http://127.0.0.1:8080/style.css messages
  • It requires a running web server to respond to the dummy requests
  • It's conceptually wrong — we're making requests we don't need

Proposed solutions

Option A: New conditional fetch function

SET response = sqlpage.fetch_if($cached IS NULL, json_object(...));

Only executes sqlpage.fetch_with_meta() when the first argument is TRUE.

Option B: Lazy evaluation for SET

SET response = LAZY sqlpage.fetch_with_meta(...);

The function is only evaluated when $response is first accessed.

Option C: Built-in cache helper

SET response = sqlpage.cached_fetch(
    cache_key => 'google_rating',
    cache_ttl => '24 hours',
    request => json_object('method', 'GET', 'url', '...')
);

sqlpage handles caching internally.

Environment

  • sqlpage version: latest (v0.44.1)
  • OS: Linux (Docker)
  • 4 external APIs cached: Google Places, Open-Meteo, IMGW Hydro, IAU Meteors

Workaround (current)

We use the localhost fallback pattern for all 4 APIs. It works but is not ideal.

Thank you

sqlpage is amazing — this is the only issue we've encountered in production. Great work!

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions