Lewati ke konten utama
Versi: v0.0.74

v0.0.41 — Prod-blocker sweep, AI generator polish, SaaS startup fix

Released: 2026-05-04

Six-commit patch release covering two more bind-param prod blockers from the v0.0.39 family, a generic runtime guard so the class can't recur, AI dashboard-generator polish, plus a SaaS startup fix and a file-upload dataset 404 fix.

Fixes

Two more v0.0.39-family prod blockers

v0.0.40 fixed SET LOCAL with bind params. v0.0.41 closes the rest of the same family — SQLAlchemy's bind-parameter parser surprises that survived the v0.0.40 cut.

SQL comments must not contain :name bind syntax. SQLAlchemy's text() regex-scans the entire SQL string for :name bind tokens, including inside -- and /* */ comments. A descriptive comment like:

-- proj_filter expands to AND project_id = :pid

silently registered pid as a required bind. When the surrounding query was built conditionally (proj_filter = "" when no project_id), the actual SQL had no :pid token, no value got supplied, and every authed call to that endpoint 500'd with:

sqlalchemy.exc.InvalidRequestError: A value is required for bind parameter 'pid'

This bit prod — the published_assets /api/publish/pages endpoint (the SaaS mobile nav surface) shipped with this exact comment shape and 500'd for every caller on some tenants. Three offenders fixed; six more comments referencing names that already appeared in the actual SQL were defensively rewritten so the class is gone end-to-end.

Generic runtime guard. A new paas/backend/safe_sql.py module wraps sqlalchemy.text (and the two aliased imports sqlalchemy.sql.text, sqlalchemy.sql.expression.text) so every caller — existing, future, copy-pasted from external snippets — gets comment-stripped SQL before bind parsing. The wrapper:

  • Strips -- line comments and /* */ block comments before SQLAlchemy sees the string.
  • Is reload-safe (walks __wrapped__ if text is already patched).
  • Exposes the genuine constructor as safe_sql.unsafe_text so the static lint can keep detecting comment-only binds even when the patch is active (without that escape hatch the lint would tautologically pass).

The wrapper is imported before any from sqlalchemy import text line in paas/backend/main.py, saas/backend/main.py, and the two pytest conftests — so prod, dev, and tests all see the same behaviour.

The :name::type cast-form trap is also covered: :: is the Postgres cast operator, but SQLAlchemy's regex chops the trailing letter and turns :param::type into a phantom para bind. The static lint includes a sanity test that confirms it detects this shape.

SaaS vertical service startup fix

saas/backend/main.py kept importing scheduled_reports from the routers package after the file was removed in the v0.0.38 scheduler refactor. SaaS crashed on startup with:

ImportError: cannot import name 'scheduled_reports' from 'routers'

Drop the dead import. PaaS was unaffected — only the SaaS surface referenced it.

File-upload datasets 404

GET /api/datasets/{name} 404'd when manifest.json was missing. File-upload datasets are registered directly in honeyframe.datasets without a dbt model and legitimately have no manifest entry — the endpoint now treats a missing manifest as {nodes: {}, sources: {}} and falls through to the registry-based file/connector path that correctly resolves these datasets.

Symptoms before this fix: spec 58 e2e failing + every /datasets/<name> page on a project with no dbt models yet (every freshly-provisioned Cloud tenant qualified).

Features

AI dashboard generator polish

The AI dashboard generator (introduced in v0.0.38, expanded in v0.0.39 with screenshot input) gets three small additions:

  • Few-shot examples in the prompt. Example A (wide scorecard with metric_grid + section_divider + a horizontal_bar comparison) and Example B (wide-monthly LATERAL VALUES reshape into a line chart) are now embedded directly in AI_DASHBOARD_PROMPT. Concrete examples drive the LLM far more reliably than abstract rules. Without them the LLM tended to plot every metric column on wide tables, or render 12 monthly columns side by side instead of pivoting to a time series.
  • "Generate dashboard with AI" empty-state CTA on the dashboard detail page. Appears only when canEdit && cards.length === 0. Hits POST /api/dashboards/generate (bulk — 8–12 cards in one shot) instead of /ai-chat (one card at a time). Lets operators bulk-seed an empty dashboard without navigating back to the list view to use the existing AI panel.
  • Three new prompt-content invariant tests that pin the worked-examples presence and the "DO NOT copy these table names verbatim" guardrail. A future prompt edit can't silently drop the worked examples or forget to warn the LLM about the t<pid>_marts placeholder.

Upgrade notes

  • No schema migration. No new tables, no enum changes, no new .env keys.
  • If you saw /api/publish/pages 500s or /datasets/<name> 404s on v0.0.39 or v0.0.40, v0.0.41 fixes them. No operator action needed beyond the upgrade.
  • The safe_sql wrapper is transparent to existing code. No call-site changes are needed; the import order in main.py is the only thing that matters and is already correct in the tarball.
  • Self-Hosted upgrade is the standard setup-customer.sh --upgrade flow.
  • Cloud / Enterprise tiers unchanged. Phase 4 (billing, suspension actions, DNS-01 wildcard cert renewal) is still the next milestone.