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.
⚠️ Please upload report for BASE (igor/core-725-scaffolding-drill-members@845ad08). Learn more about missing BASE report.

Additional details and impacted files
@@                            Coverage Diff                             @@
##             igor/core-725-scaffolding-drill-members   #11495   +/-   ##
==========================================================================
  Coverage                                           ?   59.34%           
==========================================================================
  Files                                              ?      223           
  Lines                                              ?    17912           
  Branches                                           ?     3643           
==========================================================================
  Hits                                               ?    10629           
  Misses                                             ?     6765           
  Partials                                           ?      518           
Flag Coverage Δ
cube-backend 59.34% <100.00%> (?)

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)

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