Back to developer hub

HTTP & JSONMySQL-ready SQLPartitioned vs rollup
BLAPO · Temporal logic protocol · v2

BLAPO v2

Pick how time expresses itself to your endpoints

Switch modes—you will see aggregated vs partitioned responses follow these shapes.

Live mental model

A contiguous window rolls up cleanly. Think `startDate/endDate`: one aggregate payload or array without per-period dictionaries.

BLAPO v2 — simple manual

Backend Like A Public ORM — temporal logic protocol

BLAPO is not tied to any framework. Think of it as a recipe everyone follows: callers send ambiguous time filters; your backend normalizes into one canonical shape, then chooses rollup versus dictionary-by-period replies.

Keep this abstraction stable everywhere (Python, Node, SQL layer, edge workers)—the semantics stay identical.


Table of contents

  1. Plain overview
  2. Three modes (PRC · PRSC · MPRC)
  3. Incoming fields your API accepts
  4. Canonical internal shape
  5. Resolve once, branch clearly
  6. SQL building blocks
  7. Checklist · pitfalls · recap

Plain overview

CodeMeans
PRCOne uninterrupted window—inclusive [start … end]
PRSCHand-picked slices (months and/or calendar days)—no filler months
MPRCRanges plus isolated dates—anything “specific” forces partitioning

Rules anyone can memorize:

  1. Many input shapes ⇒ still one { "ranges": [...], "specificPeriods": [...] } after normalization.
  2. If anything lives in specificPeriods (after merges) ⇒ partitioned payload (dictionary keyed by period).
  3. Only uninterrupted ranges, no stray specifics ⇒ aggregated payload (rollup / arrays).

PRC · one continuous sweep

Query example:

GET /analytics?startDate=2025-01-01&endDate=2025-01-31

Normalized internal shape (json is the contract language—use dicts/objects in Python/JS alike):

{
  "ranges": [{ "startDate": "2025-01-01", "endDate": "2025-01-31" }],
  "specificPeriods": []
}

Aggregate response intuition:

{ "total": 100, "value": 5000 }

PRSC · hand-picked months or days

GET /analytics?specificPeriods=2025-01,2025-03,2025-05

Internal shape:

{
  "ranges": [],
  "specificPeriods": ["2025-01", "2025-03", "2025-05"]
}

Partitioned intuition:

{
  "2025-01": { "total": 30, "value": 1500 },
  "2025-03": { "total": 40, "value": 2000 },
  "2025-05": { "total": 30, "value": 1500 }
}

MPRC · mixes ranges with cherry-picks

Payload example:

{
  "ranges": [{ "start": "2025-01-01", "end": "2025-01-07" }],
  "specifics": ["2025-02-14", "2025-03-15"]
}

URL-friendly variant:

GET /analytics?temporalLogic={"ranges":[{"start":"2025-01-01","end":"2025-01-07"}],"specifics":["2025-02-14","2025-03-15"]}

Normalized internally:

{
  "ranges": [{ "startDate": "2025-01-01", "endDate": "2025-01-07" }],
  "specificPeriods": ["2025-02-14", "2025-03-15"]
}

Golden rule:

Facts after normalizationResponse style
Any surviving specificPeriodsPartitioned map
Ranges only, no specificsAggregated rollup

Incoming fields — request shape abstraction

Treat this as schema-agnostic (OpenAPI/Pydantic/Dataclass/Zod—all fine). Minimal JSON contract:

{
  "startDate": "optional ISO month or date",
  "endDate": "paired with startDate",
  "specificPeriods": "array or comma string",
  "temporalLogic": "object OR JSON string for MPRC"
}

Validators should guarantee:

  • At least one knob among startDate+endDate, specificPeriods, temporalLogic.
  • Accepted tokens: YYYY-MM or YYYY-MM-DD.
  • If temporalLogic is a string ⇒ parse JSON before use.

Helpers everyone reuses

NameResponsibility
resolve_temporal_selectionPolymorphic input ⇒ canonical { ranges, specific_periods }
normalize_dateMonth-only tokens expand to inclusive bounds
normalize_period_keyDictionary keys ⇄ SQL projections stay aligned
enumerate_dates_inclusiveTurn each range into per-day helpers when merging
filter_valid_periodsQuietly discard malformed tokens

filter_valid_periods — Python

import re

def filter_valid_periods(periods):
    out = []
    for p in periods:
        t = str(p).strip()
        if re.fullmatch(r"\d{4}-\d{2}", t) or re.fullmatch(r"\d{4}-\d{2}-\d{2}", t):
            out.append(t)
    return out

filter_valid_periods — JavaScript

function filterValidPeriods(periods) {
  return periods.map(String).map((t) => t.trim()).filter((t) =>
    /^\d{4}-\d{2}$/.test(t) || /^\d{4}-\d{2}-\d{2}$/.test(t),
  );
}

Resolution order inside one function

Regardless of runtime, honor this exact priority:

  1. MPRC — parse structured temporalLogic, map "start/end"ranges, "specifics"specificPeriods, run filter_valid_periods.
  2. PRSC — coerce CSV / JSON-ish strings ⇒ specificPeriods.
  3. PRC — derive one range when startDate + endDate survive validation.

Return meaningful structural errors (bad JSON, impossible triple-state) rather than ambiguous 500s—but feel free to silently drop stray junk months if your product prefers quiet UX.

Merge specifics with expanded ranges Python

Before SQL, unify discrete keys:

def merged_period_keys(ranges, specifics):
    keys = set(filter_valid_periods(specifics))
    for r in ranges:
        for d in enumerate_dates_inclusive(r["startDate"], r["endDate"]):
            keys.add(d)
    return sorted(keys)

Same helper JavaScript

function mergedPeriodKeys(ranges, specifics) {
  const keys = new Set(filterValidPeriods(specifics));
  for (const r of ranges) {
    for (const d of enumerateDatesInclusive(r.startDate, r.endDate)) {
      keys.add(d);
    }
  }
  return [...keys].sort();
}

Implement enumerate_dates_inclusive once per runtime—pure date math only.


HTTP handler sketch (agnostic frameworks)

Python (Flask-style)

@app.get("/analytics")
def analytics_metrics():
    qs = flask.request.args
    selection = resolve_temporal_selection(dict(qs))
    return metrics_service.rollups(selection)

JavaScript (Express-style)

app.get("/analytics", async (req, res) => {
  const selection = resolveTemporalSelection(req.query);
  res.json(await metricsService.rollups(selection));
});

The only coupling is { ranges, specificPeriods } fed into whatever layer hits SQL.


SQL predicates — Python MySQL-compatible sketch

Dialects differ (DATE_TRUNC vs DATE_FORMAT). Keep argument ordering deterministic.

def build_temporal_where(column_expr, selection):
    parts, values = [], []

    if selection.get("ranges"):
        for r in selection["ranges"]:
            parts.append(f"({column_expr} BETWEEN ? AND ?)")
            values.extend([r["startDate"], r["endDate"]])

    specs = filter_valid_periods(selection.get("specificPeriods") or [])
    months = [p for p in specs if len(p) == 7]
    days = [p for p in specs if len(p) == 10]

    if months:
        placeholders = ", ".join(["?"] * len(months))
        parts.append(f"(DATE_FORMAT({column_expr}, '%Y-%m') IN ({placeholders}))")
        values.extend(months)

    if days:
        placeholders = ", ".join(["?"] * len(days))
        parts.append(f"(DATE({column_expr}) IN ({placeholders}))")
        values.extend(days)

    if not parts:
        return "(1 = 0)", []

    return "(" + " OR ".join(parts) + ")", values

Portable idea: whichever ORM/driver you ship, reuse the same branching logic when emitting SQL text.

Partitioned aggregates need a deterministic period_key expression—reuse the month/day split intuition from the prose above (COALESCE month vs day keys in MySQL, date_trunc/CASE in Postgres).


Patterned partitioned query SQL outline

SELECT
    /* period_key expression */
    SUM(amount) AS value
FROM fact_table
WHERE ( /* temporal OR-clauses */ )
  AND /* tenant predicates */
GROUP BY period_key
ORDER BY period_key;

Guard rails:

  • Never emit empty IN ()—fallback to '1 = 0'.
  • Bind parameters in documented order beside the query string.

Checklist · keep teams aligned

Before coding

  • Which URLs/bodies ingest time filters
  • Canonical timestamp column name
  • Target response shape (single, array, keyed map)

HTTP layer

  • Parses query/json into plain dict—not ad-hoc string splits everywhere
  • Validates structural mistakes loudly, cosmetic typos softly (policy choice)

Service layer

  • Single resolve_temporal_selection entry point
  • Runs merged_period_* helpers when MIX inputs exist
  • Chooses partitioned path when len(specificPeriods) > 0 after merges

Storage layer

  • Shared temporal WHERE builder
  • Optional GROUP BY period_key path
  • Legacy methods refit to { ranges, specificPeriods } instead of positional startDate/endDate only

Pitfalls condensed

  1. Placeholder drift — document bind order beside SQL.
  2. Empty IN () — guard with tautological false clauses.
  3. Mixed key shapes (2025-01 vs 2025-01-15) — normalize before dictionaries.
  4. Dropped empty buckets — pre-seed map keys users expect (0).
  5. Time zones — state UTC vs warehouse-local explicitly everywhere.

Summary sentence

Normalize every caller into { ranges, specificPeriods }, branch partition vs aggregate, keep SQL builders symmetrical with bind lists, expose thin HTTP wrappers (Python, JavaScript, or anything else)—the abstraction is temporal intent, never a single vendor runtime.