Skip to content

fix(schema-compiler): quote scaffolded YAML values that would not round-trip - #11495

Open
igorlukanin wants to merge 3 commits into
igor/core-725-scaffolding-drill-membersfrom
igor/core-747-quote-structural-yaml-values
Open

fix(schema-compiler): quote scaffolded YAML values that would not round-trip#11495
igorlukanin wants to merge 3 commits into
igor/core-725-scaffolding-drill-membersfrom
igor/core-747-quote-structural-yaml-values

Conversation

@igorlukanin

Copy link
Copy Markdown
Member

Summary

  • escapedValue decided quoting from /[{}"]/ (plus ,[] inside a flow sequence), which leaves several characters that change how YAML parses a scalar emitted bare. This is reachable from ordinary warehouse data, not just from a cast: a dimension's title is inflection.titleize(column.name), so a column named revenue: usd renders title: Revenue: Usd and the generated model file fails to parse at all, while price # net silently truncates the title to Price.
  • Replaces the character blocklist with a needsQuotes predicate covering the cases that demonstrably do not round-trip: a leading indicator character, : or a trailing colon, #, surrounding whitespace, a tab or newline, and a scalar YAML would resolve to null / a boolean / a number / a Date rather than a string. The existing {}" rule and the flow-sequence ,[] rule are kept.
  • Also escapes backslashes in the quoted form. C:\path used to be emitted as "C:\path", which is an invalid escape and a parse error; tab\there became a real tab.
  • Deliberately narrow in the other direction — a#b, a:b, COUNT(*), users.id, Order Status and yes stay unquoted, so ordinary member names, titles and SQL expressions are untouched. All 18 existing snapshots are unchanged.

Stacked on the drill_members branch, which introduced the flow-context flag this extends; it targets that branch rather than master.

Test plan

  • 11 new unit tests in scaffolding-template.test.ts, asserting the parsed document rather than the rendered text — the defect is that the output parses as something other than the string that went in, which a text assertion cannot see
  • Each new test verified red against the previous rule and green with the fix
  • Round-trip probe over 44 hostile values across all three emitted contexts (block scalar, flow sequence, block sequence)
  • Existing scaffolding snapshots unchanged (18/18), full file suite 42/42
  • tsc --noEmit and eslint clean on both changed files
  • CI must pass

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @igorlukanin's task in 5m 46s —— View job


Review: solid fix, one gap on the adjacent path — 1 medium-high, 1 medium, 3 low

The diagnosis is right and the direction is right: replacing a character blocklist with a round-trip predicate is the correct shape for this, the negative cases (a#b, a:b, COUNT(*), yes) are deliberately and correctly left alone, and asserting the parsed document rather than the rendered text is the only way these tests would have caught the original defect. The backslash escape is a real second bug found along the way. Main finding: drill_members bypasses the new predicate entirely, so the type-shaped half of the fix doesn't reach the flow sequence this PR is stacked on.

Findings and review notes — 5 inline comments
  • Gather context (diff, base branch, actual YAML consumer)
  • Review YamlSchemaFormatter.ts quoting logic
  • Review test coverage
  • Check for stale prior review threads (none exist on this PR)
  • Post review
# Severity Where Issue
1 Medium-high YamlSchemaFormatter.ts:69, flagged at :169 MemberReference returns value.member verbatim, before escapedValue. drill_members is its only producer and renders into a flow sequence, and memberName() sanitises structural characters but not type-shaped ones — so a varchar column named 2024 yields name: "2024" (quoted by this PR) alongside drill_members: [… , 2024] (a number). The drill reference no longer denotes the dimension. Same for null, true, 0x1f, 1e5.
2 Medium YamlSchemaFormatter.ts:155 needsQuotes detects [\n\t], but the quoted form escapes only \ and ", so a literal LF is still emitted inside the quotes — continuation line at column 0, js-yaml fails the document (and even indented, YAML folds the break to a space). \r isn't detected mid-string at all. Not a regression, but a case the new predicate advertises and doesn't deliver.
3 Low YamlSchemaFormatter.ts:17-18 The per-line `
4 Low YamlSchemaFormatter.ts:9-13 Comment accuracy: sexagesimal (1:30) is a YAML 1.1 integer form that isn't covered, and the "each test demonstrably does not round-trip" claim over-states the first-position set (-abc, ?abc, :abc are legal plain scalars — over-quoting is the safe direction, so code is fine). Also a suggestion to derive the type half from YAML.load instead of a parallel regex, to remove drift risk against the loader YamlCompiler.ts:90 actually uses.
5 Low scaffolding-template.test.ts:633 The yes case is the only text assertion in the new block, and it's the one whose correctness depends on the loader's schema version rather than on the formatter — toContain('name: yes') passes just as well under a 1.1 loader that returns true. Also: all 11 new cases exercise block context only; nothing covers a type-shaped element in a flow sequence.

Verified while reviewing

  • The actual consumer is js-yaml ^4.1.0 (packages/cubejs-schema-compiler/package.json:51) via YamlCompiler.ts:90, so the core schema applies and the yes decision is correct today. Worth knowing that the yaml package used in converters/ is a second, independent parser.
  • Snapshot stability is plausible on inspection: primary_key: true and other non-strings return early from escapedValue, {CUBE}.x was already quoted by the pre-existing {} rule, and no ordinary emitted value (public.orders, COUNT(*), Order Status, default) trips a new branch.
  • Nit, optional: needsQuotes touches no instance state, so it could sit at module level next to YAML_TYPE_SHAPED.
  • No performance or security surface here — six anchored regexes per scalar on a one-shot scaffolding path.

What I could not verify: node_modules is absent in this checkout and dependency installation isn't available to me, so I did not run the suite, tsc, or eslint, and findings 1–2 are derived from reading the renderer and the YAML spec rather than from an executed repro. The failure for #1 is worth confirming with an actual generateFilesByTableNames call on a table with a column named 2024.

· branch [`igor/core-747-quote-structural-yaml-values`](https://github.com/cube-js/cube/tree/igor/core-747-quote-structural-yaml-values)

Comment thread packages/cubejs-schema-compiler/src/scaffolding/formatters/YamlSchemaFormatter.ts Outdated
Comment thread packages/cubejs-schema-compiler/src/scaffolding/formatters/YamlSchemaFormatter.ts Outdated
@github-actions github-actions Bot added the javascript Pull requests that update Javascript code label Aug 6, 2026
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @igorlukanin's task in 3m 11s —— View job


All five prior findings addressed; JSON.stringify is the right escaper — 1 nit

Review of 74407ec — what changed, what I verified, 1 inline comment
  • Read 74407ec (member references, cube names, control characters)
  • Re-check each prior finding against the current diff
  • Trace the remaining unescaped paths (object keys, joins, sql_table, filenames)
  • Check for stale threads (all five already resolved by the author)
  • Post review

Prior findings

# Prior finding Status in 74407ec
1 MemberReference bypassed needsQuotes, so drill_members: [2024] stopped denoting name: "2024" Fixed — routed through escapedValue(value.member, flow) (YamlSchemaFormatter.ts:78), with a parametrised test that compares the parsed drill list to the parsed dimension names tagged by typeof, which is the only way the assertion can tell 2024 from "2024". That detail is the test doing real work.
2 Newline detected but not escaped; \r not detected mid-string Fixed, and better than suggestedJSON.stringify replaces the hand-rolled replace chain, and detection widened to [\x00-\x1f\x7f-\x9f] plus surrogates.
3 Empty alternative in YAML_TYPE_SHAPED was accidental Fixed — comment on line 18.
4 Comment over-claimed (sexagesimal, first-position set) Fixed — both claims now scoped correctly, and the sexagesimal note is right: js-yaml 4's float resolver dropped the :-separated form. The YAML.load-derived alternative was declined; fine either way, and the hand-rolled version keeps the formatter free of a parse dependency.
5 yes asserted on text only Fixed — parse assertion added alongside.

On JSON.stringify as the escaper

This is the right call and worth stating explicitly, since it looks like a shortcut and isn't. YAML's double-quoted style is a strict superset of JSON's: every escape JSON.stringify can emit (\" \\ \b \f \n \r \t \uXXXX) is a valid YAML escape, and ES2019 well-formed stringify escapes lone surrogates rather than emitting them raw — which is what makes the [\uD800-\uDFFF] branch actually deliver instead of just detecting.

Verified while reviewing

  • No unescaped dynamic path left that I could find. Object keys are emitted raw in render(), but every dynamic name reaches the output as a value: dimension and measure names via the entries.map(([ok]) => ({ name: ok, … })) conversion, and joins the same way — so a cube named 2024 joined from another file renders - name: "2024" there too, consistent with renderFile. The only raw keys are literals (cubes, dimensions, sql, type).
  • Cube-name quoting is self-consistent with {} interpolation. cubeReference produces {2024}.id, which the pre-existing {} rule already quotes, so the join SQL and the cube name agree on the string.
  • A pre-existing bug fell out of this too: sqlForMember on a column needing driver quoting produced {CUBE}."2024", which the old {}-only rule quoted without escaping — "{CUBE}."2024"", a parse error. Escaping fixes that path as a side effect.
  • Snapshots: no __snapshots__ changes in the diff, and no ordinary emitted value (public.orders, COUNT(*), Order Status) trips a new branch — consistent with the claim that all 18 are unchanged.
  • No security or performance surface: seven anchored regexes per scalar on a one-shot scaffolding path.

Nit (1 inline comment)

U+FFFE/U+FFFF are in js-yaml's non-printable set alongside the C1 range this PR now handles, so they're still emitted bare and fail the whole stream. One character to close; details inline. I also suggested pinning down why the C1 half works via quoting rather than escaping, since that's the kind of subtlety a later "simplification" removes.

What I could not verify

node_modules is absent in this checkout and dependency installation isn't available to me, so I did not run the suite, tsc, or eslint. The js-yaml behaviour above (the PATTERN_NON_PRINTABLE set, the JSON-character check in captureSegment, the 4.x int/float resolvers) is read from the 4.x loader source rather than observed in a run. The tests assert parsed documents, so CI is the real check here — in particular dimensionOf('2026-08-06').title depends on inflection.titleize leaving the dashes alone, which I couldn't confirm.

· branch [`igor/core-747-quote-structural-yaml-values`](https://github.com/cube-js/cube/tree/igor/core-747-quote-structural-yaml-values)

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.46%. Comparing base (aca6b3d) to head (a387263).

Additional details and impacted files
@@                             Coverage Diff                             @@
##           igor/core-725-scaffolding-drill-members   #11495      +/-   ##
===========================================================================
+ Coverage                                    59.45%   59.46%   +0.01%     
===========================================================================
  Files                                          223      223              
  Lines                                        17961    17963       +2     
  Branches                                      3643     3643              
===========================================================================
+ Hits                                         10678    10682       +4     
+ Misses                                        6766     6764       -2     
  Partials                                       517      517              
Flag Coverage Δ
cube-backend 59.46% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @igorlukanin's task in 1m 18s —— View job


e92c9bc closes the last open point — no findings; ready from my side

Review of e92c9bc — what changed, what I verified, why I found nothing new
  • Read e92c9bc (U+FFFE/U+FFFF)
  • Re-check all six prior threads against the current file
  • Trace remaining unescaped paths (object keys, cube name, joins, sql_table)
  • Audit the detection set against js-yaml 4's non-printable / line-break rules
  • Check stale threads (all six already resolved by the author — nothing to resolve)
  • Post review

The change

One character added to the non-printable class and two cases added to the parametrised parse test. The comment rewrite around it is the more valuable half: it now states which half of the fix does the work for which range —

Below \x20, JSON.stringify escapes … From \x7f up it does not escape, so those survive on the quoting.

That's the exact subtlety a later "why are we quoting when we already escape?" simplification would remove, and it's now written down at the line that depends on it.

Prior findings

# Prior finding Status
1 MemberReference bypassed needsQuotes Fixed in 74407ec (YamlSchemaFormatter.ts:81)
2 Newline detected, not escaped; \r undetected Fixed in 74407ec via JSON.stringify
3 Empty alternative accidental Fixed (:19)
4 Comment over-claimed Fixed (:13, :181)
5 yes asserted on text only Fixed (:633)
6 U+FFFE/U+FFFF uncovered Fixed here (:209)

Verified while reviewing

  • The detection set now matches js-yaml 4's PATTERN_NON_PRINTABLE exactly, and is a strict superset. \x85 (NEL) is excluded from js-yaml's set but included by \x7f-\x9f here — over-quoting, which is the safe direction, and harmless since is_EOL in js-yaml 4 is 0x0A/0x0D only, so a quoted \x85 stays literal. Astral noncharacters (U+1FFFE etc.) aren't in js-yaml's set either, so their absence here isn't a gap.
  • No u flag on /[\uD800-\uDFFF]/ is correct, not an oversight — without it the class matches individual code units, which is what unpaired-surrogate detection needs.
  • U+2028/U+2029 are safe unhandled. JSON.stringify doesn't escape them, but js-yaml 4 follows YAML 1.2 where they aren't line breaks, so they round-trip bare. (Would be a bug under a 1.1 loader — noting it only because the file's other decisions are pinned to js-yaml 4 as well.)
  • No unescaped dynamic path remains. Every dynamic name reaches the output as a value: members and joins via the entries.map(([ok]) => ({ name: ok, … })) conversion, the cube name via the explicit escapedValue in renderFile:56. The only raw keys in render() are literals.
  • The drill-member test does real work. Tagging each parsed value with typeof before comparing as sets is the only formulation that distinguishes 2024 from "2024" — a plain sorted comparison would pass on the broken output.

Nits, all optional and none blocking

  • needsQuotes touches no instance state; it could sit at module level next to YAML_TYPE_SHAPED.
  • The �� literals are invisible in the source; a -style spelling would read closer to the comment above them. (The current form is already the escaped spelling — noting only that the uppercase-hex convention used for \uD800 two characters later differs.)

What I could not verify

node_modules is absent in this checkout and dependency installation isn't available to me, so I did not run the suite, tsc, or eslint. The js-yaml behaviour cited above is read from the 4.x loader source, not observed in a run — CI is the real check, and the tests assert parsed documents, which is the right thing for it to check.

· branch [`igor/core-747-quote-structural-yaml-values`](https://github.com/cube-js/cube/tree/igor/core-747-quote-structural-yaml-values)

@igorlukanin
igorlukanin force-pushed the igor/core-725-scaffolding-drill-members branch from 845ad08 to aca6b3d Compare August 8, 2026 05:29
@igorlukanin
igorlukanin force-pushed the igor/core-747-quote-structural-yaml-values branch from e92c9bc to a387263 Compare August 8, 2026 05:30
@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @igorlukanin's task in 2m 7s —— View job


Rebase only — content identical to the state I already reviewed; no new findings

Review of the synchronize push (a387263) — what I checked and why nothing changed
  • Gather context (diff vs origin/igor/core-725-scaffolding-drill-members, commit list, base tip)
  • Re-read YamlSchemaFormatter.ts in full against the current base
  • Re-read the test additions
  • Re-audit the predicate against ordinary emitted values
  • Check review threads (all six already resolved by the author — nothing stale to resolve)
  • Post review

What this push is

The base branch advanced to aca6b3d (quote YAML flow-sequence items containing a comma or bracket) and the three commits were replayed on top of it:

f2c2e04 quote scaffolded YAML values that would not round-trip
be95f27 quote member references and cube names, escape control characters
a387263 quote the U+FFFE/U+FFFF noncharacters too

Same three subjects, same order as f2c2e04/74407ec/e92c9bc before the rebase, and the resulting file matches the state described in my last review character for character — MemberReference routed through escapedValue(value.member, flow) (:81), JSON.stringify as the escaper (:171), the deliberate-empty-alternative comment (:19), the noncharacters in the detection class (:209), and the split explanation at :200-207. The diff is +87/-6 and +169/-0, unchanged. (I can't diff against the pre-rebase SHAs directly — they aren't in this checkout — so this is an inspection of the resulting tree, not a machine comparison.)

Re-verified against the new base

The base version of the formatter I read this time is a clean subset: renderFile still interpolates ${cube} raw, MemberReference still returns value.member, and escapedValue is still the two-regex /[{}"]/ + flow /[{}",[\]]/ form with a "-only replace. So the rebase introduced no overlap or partial duplication with the flow-sequence commit the base now carries — this PR strictly extends it, and the flow threading it relies on is the base's, untouched.

Re-audited the predicate (no execution)

Walked the ordinary emitted values through needsQuotes by hand again against the current source; none trip a new branch, consistent with the unchanged snapshots:

  • Order Status, COUNT(*), public.orders, users.id, default, id — no indicator in first position, no : / #, no type shape.
  • 1.2.3 — the decimal alternative anchors at $, so 1.2 + .3 doesn't match. Stays a string, correctly unquoted.
  • 12:30:00 — every colon is followed by a digit, so /:(?:\s|$)/ misses it and it stays bare; correct under js-yaml 4, which resolves a timestamp only with a date part.
  • 2024-13-45 and 08 are over-quoted (invalid month, leading-zero decimal). Safe direction, no behaviour change.
  • Astral characters (emoji, CJK ext) match [\uD800-\uDFFF] as a well-formed pair and get quoted — already called out in the comment at :206 as costing only a pair of quotes.

What I could not verify

node_modules is absent in this checkout and dependency installation isn't available to me, so I did not run the suite, tsc, or eslint — CI is the real check. The one assertion I still can't confirm statically is dimensionOf('2026-08-06').title depending on inflection.titleize leaving the dashes alone.

Nothing blocking from my side; the same two optional nits from last time stand (needsQuotes could sit at module level; the �� literals are invisible in source next to the \uD800 spelling two characters later).

· branch [`igor/core-747-quote-structural-yaml-values`](https://github.com/cube-js/cube/tree/igor/core-747-quote-structural-yaml-values)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant