Volver al hub para desarrolladores

HTTP y JSONSQL listo para MySQLParticionado vs agregación
BLAPO · Protocolo temporal · v2

BLAPO v2

Elige cómo llega la dimensión temporal a tus endpoints

Cambia de modo — verás cómo cambian respuestas agregadas frente a diccionarios por período.

Modelo en vivo

Un intervalo corrido tiende al rollup único (`start/end`): sin llaves intermedias cuando solo cuentas el bloque.

BLAPO v2 — manual sencillo

Backend como un ORM público — protocolo de lógica temporal

BLAPO no está atado a ningún framework. Piénsalo como una receta que todos siguen: el cliente envía filtros temporales ambiguos; tu backend los normaliza a una sola forma canónica y luego decide entre respuesta tipo rollup o mapa por periodo.

Mantén esta abstracción estable en todas partes (Python, Node, capa SQL, edge workers)—la semántica es idéntica.


Tabla de contenidos

  1. Vista general
  2. Tres modos (PRC · PRSC · MPRC)
  3. Campos de entrada que acepta tu API
  4. Forma interna canónica
  5. Resolver una vez, ramificar sin ambigüedad
  6. Piezas SQL de apoyo
  7. Lista · trampas · cierre

Vista general

CódigoSignifica
PRCUna ventana continua inclusiva [inicio … fin]
PRSCRebanadas elegidas a mano (meses y/o días)—sin meses relleno
MPRCRangos más fechas sueltas—cualquier “específico” fuerza partición

Reglas memorables:

  1. Muchos formatos de entrada ⇒ solo una { "ranges": [...], "specificPeriods": [...] } después de normalizar.
  2. Si algo queda en specificPeriods (tras merges) ⇒ payload particionado (diccionario con clave por periodo).
  3. Solo rangos continuos, sin específicos ⇒ payload agregado (rollup / arreglos).

PRC · barrido continuo

Ejemplo de consulta:

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

Forma interna normalizada (el idioma del contrato es json; en Python/JS usa dict/objeto):

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

Intuición de respuesta agregada:

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

PRSC · meses o días puntualmente elegidos

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

Forma interna:

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

Intuición particionada:

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

MPRC · combina rangos con fechas puntual

Ejemplo de payload:

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

Variante apta para URL:

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

Normalizado internamente:

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

Regla de oro:

Después de normalizarEstilo de respuesta
Persiste cualquier specificPeriodsMapa particionado
Solo rangos, sin específicosRollup agregado

Campos de entrada — abstracción de la forma de la petición

Trátalo como agnóstico de schema (OpenAPI/Pydantic/Dataclass/Zod—todo válido). Contrato JSON mínimo:

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

Los validadores deben garantizar:

  • Al menos un control entre startDate+endDate, specificPeriods, temporalLogic.
  • Tokens YYYY-MM o YYYY-MM-DD.
  • Si temporalLogic es cadena ⇒ parsear JSON antes de usar.

Helpers reutilizables

NombreResponsabilidad
resolve_temporal_selectionEntrada polimórfica ⇒ canónico { ranges, specific_periods }
normalize_dateTokens mes-only expanden límites inclusivos
normalize_period_keyLlaves de diccionario ⇄ proyección SQL
enumerate_dates_inclusiveRangos ⇄ helpers por día al fusionar
filter_valid_periodsDescarta tokens mal formados en silencio

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),
  );
}

Orden de resolución en una función

Sin importar el runtime, mantén esta prioridad:

  1. MPRC — parsea temporalLogic; mapea "start/end"ranges, "specifics"specificPeriods; ejecuta filter_valid_periods.
  2. PRSC — CSV / cadenas tipo JSON ⇒ specificPeriods.
  3. PRC — un rango si startDate + endDate sobreviven validación.

Devuelve errores de estructura claros (JSON mal, triple estado absurdo) en lugar de 500 opacos—pero puedes silenciar meses basura si el producto prefiere UX silenciosa.

Fusionar specifics con rangos expandidos Python

Antes del SQL unifica llaves:

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)

La misma pieza 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();
}

Implementa enumerate_dates_inclusive una vez por runtime—solo aritmética de fechas.


Esquema de handler HTTP (agnóstico de framework)

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));
});

El único acoplamiento es { ranges, specificPeriods } hacia la capa que toca SQL.


Predicados SQL — esbozo Python compatible MySQL

Los dialectos difieren (DATE_TRUNC vs DATE_FORMAT). Mantén el orden de argumentos determinista.

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

Idea portable: el ORM/driver que uses, reutiliza el mismo ramificado al emitir SQL.

Los agregados particionados necesitan period_key determinista—reutiliza la intuición mes/día del texto (COALESCE en MySQL, date_trunc/CASE en Postgres).


Consulta particionada — esquema SQL

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;

Vallas:

  • Nunca emitas IN () vacío—usa '1 = 0'.
  • Parámetros enlazados en orden documentado junto a la consulta.

Lista de verificación · alinear equipos

Antes de codificar

  • Qué URLs/cuerpos ingieren filtros temporales
  • Nombre canónico de columna timestamp
  • Forma de respuesta objetivo (single, array, mapa con clave)

Capa HTTP

  • Parsea query/json a dict plano—no trocear strings ad hoc en todos lados
  • Errores estructurales en voz alta, typos cosméticos suave (política)

Capa servicio

  • Un solo punto de entrada resolve_temporal_selection
  • Helpers merged_period_* cuando haya entradas mixtas
  • Ruta particionada si len(specificPeriods) > 0 tras merges

Capa almacenamiento

  • Constructor compartido de WHERE temporal
  • Ruta opcional GROUP BY period_key
  • Métodos legacy adapados a { ranges, specificPeriods }

Trampas frecuentes

  1. Orden binders ≠ orden docs — documenta el orden junto al SQL.
  2. IN () vacío — protege con tautología falsa.
  3. Formas mezcladas (2025-01 vs 2025-01-15) — normaliza antes del mapa.
  4. Buckets vacíos perdidos — pre-rellena claves esperadas (0).
  5. Zona horaria — deja UTC vs warehouse-local explícito.

Frase final

Normaliza cualquier entrada a { ranges, specificPeriods }, bifurca partición vs agregación, mantén builders SQL simétricos con listas de bind y expón handlers HTTP finos (Python, JavaScript, u otros)—la abstracción es intención temporal, jamás el runtime del proveedor.