UNPKG

miniml

Version:

A minimal, embeddable semantic data modeling language for generating SQL queries from YAML model definitions. Inspired by LookML.

45 lines (30 loc) 8.1 kB
# WAVE 1 — THE PRINCIPAL ENGINEER > Reconstructed brief. The originally-dispatched subagent confirmed the load-bearing finding (dead-code AST validation) before a session-limit API error terminated it; this brief re-derives every claim from direct reads of lib/*.ts and executed probes by the Orchestrator acting under the protocol's sequential-fallback clause. All [MEASURED] tags reflect code actually read or commands actually run. ## Position (2 sentences) MiniML is a clean, readable ~1,040-LOC compiler whose core idea (deterministic YAML→SQL with a validated freeform-filter escape hatch) is sound and separable from its flaws, but it is built on string concatenation with a per-dialect string-templating layer, and its single most-advertised safety mechanism — AST-based allowlist validation — is dead code that never executes. The engineering is a competent one-person weekend-scale library, honestly documented down to its own bug list, but it is not production-grade for untrusted/LLM-driven input today. ## Evidence (file-by-file) **`lib/validation.ts` — the load-bearing defect. [MEASURED]** `validateSqlExpression` calls `parser.astify(\`SELECT * FROM dummy WHERE ${expression}\`)` (line 92), then hands the result to `validateAstSafety` (line 100). But `validateAstSafety` only descends into the AST if `Array.isArray(ast)` (line 280). I verified with node-sql-parser directly: for a single statement, `astify` returns an **object**, not an array (`Array.isArray(ast) === false`, `ast.where` present). Therefore `validateNode` never runs and `validateAstSafety` **always returns `{ok:true}`**. The function allowlist (`SAFE_FUNCTIONS`), the AST-level statement-type blocks, and the node-count/depth limits are all inert. Proof: `validateWhereClause("SESSION_USER() = revenue", model)` returns **ok** — a function absent from the allowlist passes. If the AST layer ran, it would be rejected with "Function 'SESSION_USER' is not allowed." [MEASURED — executed probe] What still protects the query is the *regex* layer `performBasicSafetyChecks` (lines 148–208), which blocks `SELECT`, `UNION`, DDL/DML keywords, and comments by string match. So the crude guard works; the sophisticated one the README sells does not. **`lib/query.ts` — string-concatenation SQL + two live logic bugs. [MEASURED]** - SQL is assembled by array-join of interpolated strings (lines 101–163). Dates are regex-validated before interpolation (`validateDateInput`, strict `YYYY-MM-DD`), so date interpolation is safe; but the architecture has no query IR, so every future feature is more string surgery. - **Bug 1 (line 119):** `else if (model.default_date_range && date_from !== null && date_from !== null)` — `date_from` is tested twice; `date_to` is never tested. Consequence: `date_to: null` does **not** bypass the default date range. I confirmed the generated SQL still contains the `CURRENT_TIMESTAMP` default range. This directly contradicts the tool's own auto-generated `info` guidance ("Specify `date_to` as `null` to bypass…", written in load.ts:131) and README lines 592–593. The tool ships AI-facing instructions for a behavior it does not implement. - **Bug 2 (lines 69–72):** join resolution does `where_refs.map(key => model.dimensions[key].join!)` and `having_refs.map(key => model.measures[key].join!)`. It assumes every WHERE reference is a dimension and every HAVING reference is a measure. A WHERE clause referencing a measure key (or HAVING referencing a dimension) indexes `undefined` and throws `TypeError`. [MEASURED — read; consistent with TASKS.md] - `date_from == date_to` produces `BETWEEN 'x' AND 'x'` (line 114); harmless for DATE-typed fields, silently empty for TIMESTAMP fields — matches the open TASKS.md item. **`lib/dialect.ts` — hand-rolled per-dialect string builders. [MEASURED]** Each date/trunc/offset function is an `if (dialect==="bigquery")… else if ("snowflake")… else throw` ladder (lines 1–48). Adding a third dialect means editing every function — the author's own TASKS.md flags splitting this file. `normalizeQuotesForBigQuery` (lines 55–82) is a genuinely careful hand-written quote-state machine; it's the best code in the repo and is well-tested. **`lib/load.ts` — the nicest design work. [MEASURED]** The polymorphic dimension/measure expansion (string | array | object → normalized `MinimlDef`, lines 92–101) and the SUM-by-default measure logic are tidy and are what makes the YAML terse. `expandModelInfo` (lines 104–139) generates the LLM-facing model card and is the genuinely agent-relevant asset. It renders through nunjucks (see security note). **`lib/jinja.ts` — SSTI surface. [MEASURED]** `renderJinjaTemplate` calls `nunjucks.renderString(text, context)` with no sandbox. `info` is model-authored (trusted per the security model), so this is defense-in-depth, not an open hole today — but any future path that renders untrusted/LLM-generated model text is an injection (`{{7*7}}`→49 confirmed). **Test honesty. [MEASURED]** 68 tests, all in `test/validation.test.ts`, all green in ~111ms. They genuinely exercise the injection/validation surface hard — but they test `validateWhereClause`/`renderQuery` at the API boundary and pass *because the regex layer catches the dangerous cases in the corpus*; they do not assert that the AST layer runs, which is exactly why the dead-code defect survived. Query-generation correctness (join ordering, date math, granularity, the two bugs above) is thinly covered. `test/column-ref.test.ts` is referenced but no source is present. There is no CI to run any of it. **Bus factor / onboarding.** A motivated stranger *could* onboard from this code in a day — it's small and clear. But there is no CONTRIBUTING, no CI, and compiled `.js/.d.ts` are committed despite `.gitignore` listing them, which is a newcomer trap. [MEASURED] ## Strongest point FOR the project The best ideas are cleanly separable from the worst decisions: the terse-YAML model expansion (load.ts) and the LLM-facing model-card generation (expandModelInfo) are genuinely good and portable; they do not depend on the string-concatenation SQL layer or the broken AST validator. A rewrite that keeps `load.ts`'s shape and swaps the SQL backend for a real query builder would preserve everything valuable and discard everything fragile. ## Strongest point AGAINST The project's headline promise — "AST-based validation, allowlist approach, only safe SQL constructs permitted" (README Security section) — is not true in the shipped code: the AST validator never runs, and a non-allowlisted function call passes. For a library whose entire reason to exist is being the *safe* layer between an LLM and a database, shipping an inert version of the advertised control, undetected because the tests assert outcomes the weaker regex layer happens to satisfy, is the most serious possible class of defect for this specific product. ## Viability score: 34/100 Competent, honest, small-surface engineering with a correct core idea and separable good parts — but a false headline safety claim, string-concat architecture, two live correctness bugs, one test file, and no CI. ~2–4 engineering-months to genuinely production-grade (fix AST validation + tests that assert it runs; add a query IR or adopt a builder; broaden test coverage to generation; add CI). The code is not the reason to kill or keep this — the market and traction briefs are — but on its own merits it's a promising prototype, not a dependable dependency. ## The one question that would change my mind If I add one test — `expect(validateWhereClause("SESSION_USER() = x", model).ok).to.be.false` — does the author treat a red result as a five-minute fix (handle the object case at line 280) and add coverage, or does it sit? A fast fix with a regression test moves my score toward 50 and signals a codebase that's alive; a project where the core safety control can be inert for 13 months and through a 9-month dormancy is one whose safety guarantees I cannot trust regardless of intent.