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
- Plain overview
- Three modes (PRC · PRSC · MPRC)
- Incoming fields your API accepts
- Canonical internal shape
- Resolve once, branch clearly
- SQL building blocks
- Checklist · pitfalls · recap
Plain overview
| Code | Means |
|---|---|
| PRC | One uninterrupted window—inclusive [start … end] |
| PRSC | Hand-picked slices (months and/or calendar days)—no filler months |
| MPRC | Ranges plus isolated dates—anything “specific” forces partitioning |
Rules anyone can memorize:
- Many input shapes ⇒ still one
{ "ranges": [...], "specificPeriods": [...] }after normalization. - If anything lives in
specificPeriods(after merges) ⇒ partitioned payload (dictionary keyed by period). - 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 normalization | Response style |
|---|---|
Any surviving specificPeriods | Partitioned map |
| Ranges only, no specifics | Aggregated 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-MMorYYYY-MM-DD. - If
temporalLogicis a string ⇒ parse JSON before use.
Helpers everyone reuses
| Name | Responsibility |
|---|---|
resolve_temporal_selection | Polymorphic input ⇒ canonical { ranges, specific_periods } |
normalize_date | Month-only tokens expand to inclusive bounds |
normalize_period_key | Dictionary keys ⇄ SQL projections stay aligned |
enumerate_dates_inclusive | Turn each range into per-day helpers when merging |
filter_valid_periods | Quietly 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:
- MPRC — parse structured
temporalLogic, map"start/end"⇒ranges,"specifics"⇒specificPeriods, runfilter_valid_periods. - PRSC — coerce CSV / JSON-ish strings ⇒
specificPeriods. - PRC — derive one range when
startDate+endDatesurvive 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_selectionentry point - Runs
merged_period_*helpers when MIX inputs exist - Chooses partitioned path when
len(specificPeriods) > 0after merges
Storage layer
- Shared temporal
WHEREbuilder - Optional
GROUP BY period_keypath - Legacy methods refit to
{ ranges, specificPeriods }instead of positionalstartDate/endDateonly
Pitfalls condensed
- Placeholder drift — document bind order beside SQL.
- Empty
IN ()— guard with tautological false clauses. - Mixed key shapes (
2025-01vs2025-01-15) — normalize before dictionaries. - Dropped empty buckets — pre-seed map keys users expect (
0). - 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.