Skip to content

perf(transform): reuse compiled schema regexes and skip schemas absent from the content - #341

Merged
pyramation merged 1 commit into
mainfrom
feat/transform-regex-cache
Aug 5, 2026
Merged

perf(transform): reuse compiled schema regexes and skip schemas absent from the content#341
pyramation merged 1 commit into
mainfrom
feat/transform-regex-cache

Conversation

@pyramation

Copy link
Copy Markdown
Collaborator

Summary

The string-level passes in transform.ts (transformSchemaRefsInString, transformVerifyCalls, transformJsonStringValues, transformComments, validateNoUntransformedSchemas) each rebuild a RegExp per (file × schema) and then scan the whole file with it — even though the pattern depends only on the schema name, and the schema usually does not appear in the file at all. On a real workload (transpiling a 24.5k-change pgpm module across ~40 schemas) that is ~1M identical compiles plus ~1M full-content scans; the CPU profile is dominated by escapeRegexp / regex compilation, not by parsing.

Two changes, no behavior change:

-const pattern = new RegExp(`(?<![\w-])("?)${escapeRegexp(oldSchema)}\1(?=\.)`, 'g');
+if (!out.includes(oldSchema)) continue;
+const pattern = cachedRegExp(`(?<![\w-])("?)${cachedEscapeRegexp(oldSchema)}\1(?=\.)`, 'g');
  • cachedRegExp(source, flags) / cachedEscapeRegexp(str) memoize by pattern text, so each distinct pattern compiles once per process. The cached instances are stateful (g carries lastIndex), which is safe here because every call site uses replace/match (both reset it) or already resets lastIndex itself, as it had to with the throwaway instances.
  • A substring pre-check skips the scan entirely for schemas that cannot match: every pattern requires a literal occurrence of the schema name, so content.includes(schema) is a necessary condition. Where the pattern is case-insensitive (transformVerifyCalls, validateNoUntransformedSchemas) the guard is done on a lowercased copy, and transformVerifyCalls refreshes that copy after a replacement so a name introduced by an earlier substitution is still seen.

Measured

Transpiling application/constructive (24.5k changes, ~40 schemas) in the constructive-db codegen loop, same machine:

before after
transform + transpile phase 24.4s 11.1s

Output is byte-identical across all 24.5k generated files. @pgsql/transform tests: 236 passed, 4 snapshots; lint clean.

Link to Devin session: https://app.devin.ai/sessions/b266e1e72e0a4182a53cdd048b59f740
Requested by: @pyramation

@pyramation pyramation self-assigned this Aug 5, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

const combinedPattern = new RegExp(
const combinedPattern = cachedRegExp(
`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)|` +
`(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we not using ASTs here, and why are we using regular expressions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question — this PR doesn't introduce the regex approach, it only stops recompiling the same patterns, but the "why" is worth pinning down per call site:

validateNoUntransformedSchemas specifically cannot use the AST, because it's the safety net for the AST pass having missed a node. It runs on the deparsed output and asks "did any old schema name survive?" — checking the AST would only re-ask the question the AST pass already answered wrong, so it would pass exactly when it most needs to fail. Its own error message says as much ("indicates a missing visitor handler in create_sql_visitor").

The other three are text the SQL AST genuinely doesn't reach:

  • transformComments — pgpm -- Deploy: / -- requires: header paths; not SQL at all.
  • transformVerifyCallsverify_function('schema.fn'); the schema name lives inside a string literal, so to the parser it's opaque text, not a qualified name.
  • transformJsonStringValues — same, schema names inside JSON string values.

So the regex isn't standing in for AST work here; it covers the non-AST residue plus a post-hoc assertion. Where it would be wrong is regex-rewriting actual SQL identifiers — that stays createSqlVisitor's job.

Happy to go further separately if you want: transformVerifyCalls could parse the literal's contents (known-shape qualified name) instead of pattern-matching, and the leftover check could be narrowed to string/comment tokens via the lexer rather than scanning the whole file.

@pyramation
pyramation merged commit 67c0946 into main Aug 5, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant